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_Services_audio_drives_the_effects¶
test/scenarios/core/scenario_Services_audio_drives_the_effects.json: Audio arrives wired on every device and synthesized by default, so the effects react before a microphone is attached to anything. Proves the default is what ships, that a source change takes, and that the choice survives a restart.
Mode: mutate Β· Also touches: Services
Trailing setup (no measurement after)¶
simulate-is-the-default(expect_control): A device demonstrates sound with nothing attached, which is why this mode is first.pick-the-microphone(set_control): A board with a real microphone selects local audio, the way its catalog entry does.it-took(expect_control)pick-a-rate(set_control): The sample rate rebuilds the capture, on a computer and on a device's I2S alike.the-rate-took(expect_control)set-the-gain(set_control): Gain is a plain write rather than a rebuild: the contrast with the rate above is the point.the-gain-took(expect_control)restart(reboot): Restart, because a source that reverts leaves a mic board silently synthesizing.the-source-survived(expect_control)the-rate-survived(expect_control): A capture setting a user picked once should not need picking again.back-to-simulate(set_control): Back to the default, so the scenario leaves the device as it found it.back-to-the-default-rate(set_control): And the capture rate, which a later scenario would otherwise inherit as though a user had chosen it.back-to-the-default-gain(set_control)
AuroraEffect¶
scenario_Aurora_fps¶
test/scenarios/device/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 Β· live-only (skipped in-process) Β· 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-docker-arm64 |
β / 255 | β / β | β / β |
desktop-macos |
β₯ 333 / 833-1,160 | unlimited / β | β / β |
desktop-windows |
β / 150 | β / β | β / β |
esp32 |
β / 8.2 | β / 46KB | β / 30KB-34KB |
esp32p4rev1-eth |
β / 3.9-26.8 | β / 33078KB-33115KB | β / 252KB-280KB |
esp32p4rev1-eth-wifi |
β / 0.1-0.2 | β / 26200KB-32911KB | β / 224KB-288KB |
esp32s3-n16r8 |
β / 8.6-9.6 | β / 8209KB-8263KB | β / 72KB-92KB |
esp32s31 |
β / 20.1 | β / 16339KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-09-04 "initial contract: Aurora at 64x64, 3 layers, 2 octaves, warp on" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 653 | β / β | β / β |
desktop-macos |
β₯ 667 / 1,949-3,067 | unlimited / β | β / β |
desktop-windows |
β / 169 | β / β | β / β |
esp32 |
β / 18.9-19.0 | β / 46KB | β / 30KB-34KB |
esp32p4rev1-eth |
β / 68.7-752 | β / 33078KB-33109KB | β / 252KB-272KB |
esp32p4rev1-eth-wifi |
β / 0.1-1.4 | β / 26264KB-32911KB | β / 224KB-288KB |
esp32s3-n16r8 |
β / 14.9-15.4 | β / 8209KB-8264KB | β / 72KB-92KB |
esp32s31 |
β / 57.6 | β / 16339KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-09-04 "initial contract: one layer, roughly a third of the three-layer cost" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 919 | β / β | β / β |
desktop-macos |
β₯ 1,111 / 2,558-5,291 | unlimited / β | β / β |
desktop-windows |
β / 275 | β / β | β / β |
esp32 |
β / 45.5 | β / 46KB | β / 30KB-34KB |
esp32p4rev1-eth |
β / 99.5-412 | β / 33079KB-33086KB | β / 252KB-272KB |
esp32p4rev1-eth-wifi |
β / 1.6 | β / 26264KB | β / 224KB |
esp32s3-n16r8 |
β / 13.8 | β / 8209KB-8265KB | β / 72KB-92KB |
esp32s31 |
β / 88.3 | β / 16339KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-09-04 "initial contract: one layer without domain warping" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 322 | β / β | β / β |
desktop-macos |
β₯ 294 / 933-1,789 | unlimited / β | β / β |
desktop-windows |
β / 170 | β / β | β / β |
esp32 |
β / 13.7 | β / 47KB | β / 30KB-34KB |
esp32p4rev1-eth |
β / 28.6-37.2 | β / 33001KB-33078KB | β / 252KB-264KB |
esp32p4rev1-eth-wifi |
β / 1.3 | β / 26263KB | β / 224KB |
esp32s3-n16r8 |
β / 15.0-15.3 | β / 8210KB-8265KB | β / 72KB-92KB |
esp32s31 |
β / 32.6 | β / 16340KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-09-04 "initial contract: four layers, warp off" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 194 | β / β | β / β |
desktop-macos |
β₯ 143 / 694-1,042 | unlimited / β | β / β |
desktop-windows |
β / 175 | β / β | β / β |
esp32 |
β / 6.5 | β / 47KB-48KB | β / 30KB-34KB |
esp32p4rev1-eth |
β / 5.2-20.9 | β / 32679KB-33078KB | β / 252KB-288KB |
esp32p4rev1-eth-wifi |
β / 1.1 | β / 26260KB | β / 224KB |
esp32s3-n16r8 |
β / 8.8 | β / 8210KB-8265KB | β / 72KB-92KB |
esp32s31 |
β / 18.3 | β / 16340KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-09-04 "initial contract: four layers, four octaves, warp still off" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 133 | β / β | β / β |
desktop-macos |
β₯ 111 / 601-680 | unlimited / β | β / β |
desktop-windows |
β / 177 | β / β | β / β |
esp32 |
β / 4.7 | β / 47KB | β / 30KB-34KB |
esp32p4rev1-eth |
β / 3.5-13.9 | β / 32613KB-33078KB | β / 252KB-288KB |
esp32p4rev1-eth-wifi |
β / 0.5 | β / 26263KB | β / 224KB |
esp32s3-n16r8 |
β / 5.0-5.3 | β / 8210KB-8265KB | β / 72KB-92KB |
esp32s31 |
β / 11.4 | β / 16340KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-09-04 "initial contract: four layers, four octaves, warp on: the true ceiling" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 137 | β / β | β / β |
desktop-macos |
β₯ 105 / 502-648 | unlimited / β | β / β |
desktop-windows |
β / 166 | β / β | β / β |
esp32 |
β / 4.5-4.6 | β / 47KB-48KB | β / 30KB-34KB |
esp32p4rev1-eth |
β / 12.0 | β / 33078KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 0.6 | β / 26264KB | β / 224KB |
esp32s3-n16r8 |
β / 4.4-5.4 | β / 8210KB-8265KB | β / 72KB-92KB |
esp32s31 |
β / 12.1 | β / 16340KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-09-04 "initial contract: the fold adds one modulo per pixel, not a pass" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 191 | β / β | β / β |
desktop-macos |
β₯ 238 / 736-882 | unlimited / β | β / β |
desktop-windows |
β / 178 | β / β | β / β |
esp32 |
β / 5.4 | β / 47KB-48KB | β / 30KB-34KB |
esp32p4rev1-eth |
β / 19.5 | β / 33078KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 0.7 | β / 26262KB | β / 224KB |
esp32s3-n16r8 |
β / 7.4 | β / 8210KB-8265KB | β / 72KB-92KB |
esp32s31 |
β / 15.1 | β / 16340KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-09-04 "initial contract: four layers, two octaves, folded" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
ControlModule¶
scenario_Control_presets_capture_and_restore¶
test/scenarios/core/scenario_Control_presets_capture_and_restore.json: A preset captures what the device is doing and puts it back later, which is the only feature that reaches across Layouts, Effects, Drivers and Services at once. Proves a surface control drives a real target rather than only moving on screen.
Mode: mutate Β· Also touches: Drivers
Trailing setup (no measurement after)¶
aim-a-fader(set_control): Point fader 1 at the global brightness, which is the shortest path from a surface to visible output.the-aim-took(expect_control)move-it(set_control): Move the fader, which should drive the target rather than merely record a number.the-target-followed(expect_control): The brightness is what the fader said, so the surface reaches the driver.restart(reboot): Restart: a surface that forgets what it was aimed at is a surface set up twice.the-aim-survived(expect_control)
Drivers¶
scenario_Drivers_output_and_brightness¶
test/scenarios/light/scenario_Drivers_output_and_brightness.json: How a finished frame leaves the device. The global brightness and the master switch are the two controls every installation touches, and both must apply on the next frame rather than at the next boot. Brightness defaults low so a fresh board on USB cannot brown out at full white, which is a safety default worth pinning.
Mode: mutate Β· Also touches: PreviewDriver, LightPresetsModule
measure-bright (measure) π¶
Setup (preceding non-measured steps):
- brightness-starts-low (expect_control): A fresh device is dim on purpose: full white on USB 5V browns a board out. The reset block above is what makes this readable after another scenario has run.
- turn-it-up (set_control): What a user does once the board is powered properly.
- it-took (expect_control)
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 4,587 | β / β | β / β |
desktop-macos |
β / 2,525-100,000 | β / β | β / β |
desktop-windows |
β / 433-250,000 | β / β | β / β |
esp32s3-n16r8 |
β / 115 | β / 8282KB | β / 60KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-30desktop-windows: observed 2026-09-30esp32s3-n16r8: observed 2026-09-30
Trailing setup (no measurement after)¶
master-off(set_control): The master switch blacks the output without touching brightness, so toggling back restores the level.it-is-off(expect_control)the-level-was-kept(expect_control): Brightness is untouched by the switch, which is why toggling back does not lose the setting.master-on(set_control)restart(reboot): Restart: an installation's brightness is set once and expected to stay.brightness-survived(expect_control)
Effects¶
scenario_Effects_pipeline_builds_and_renders¶
test/scenarios/light/scenario_Effects_pipeline_builds_and_renders.json: The whole light pipeline from nothing: a layout says where the lights are, a layer says what they show, a driver says how the frame leaves. Builds each in turn and measures after each, so the cost of one part is the difference between two steps rather than a number with nothing to compare it to. This is the path every other light scenario assumes, which is why it is the one that builds it explicitly.
Mode: construct Β· Also touches: Layouts, GridLayout, Layer, Drivers, PreviewDriver
measure-empty (measure) π¶
A layer with no effect: the floor every later measurement is read against.
Setup (preceding non-measured steps):
- layouts (add_module): The container the layouts live in, which the layer reads its coordinates from.
- grid (add_module): A 16x16 panel: small enough to run anywhere, big enough that a per-light cost is visible.
- layer (add_module): One layer, wired to the layouts it takes coordinates from, which is what an effect renders into.
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-macos |
β / 1,000,000 | β / β | β / β |
desktop-macos: observed 2026-09-29
measure-rendering (measure) π¶
The difference against the empty layer is what one effect costs at this size.
Setup (preceding non-measured steps):
- effect (add_module): A gradient, chosen because it paints from the first frame: this scenario measures the pipeline rather than an effect, and an effect that needs time to develop would make a black buffer look like a broken pipeline.
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-macos |
β / 250,000-500,000 | β / β | β / β |
desktop-windows |
β / 250,000-333,333 | β / β | β / β |
desktop-macos: observed 2026-09-30desktop-windows: observed 2026-09-30
measure-complete (measure) π¶
The whole pipeline: layout, layer, effect, driver. The difference is what output costs.
Setup (preceding non-measured steps):
- drivers (add_module): The drivers container, wired to the effects whose buffer it reads, again mirroring boot.
- preview (add_module): The preview is a driver like any other, and the one every target has.
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-macos |
β / 23,810-142,857 | β / β | β / β |
desktop-windows |
β / 166,667-250,000 | β / β | β / β |
desktop-macos: observed 2026-09-30desktop-windows: observed 2026-09-30
Trailing setup (no measurement after)¶
the-grid-is-what-was-asked-for(expect_control): The layout reports the size it was built with, so a later resize has something to differ from.
scenario_Effects_swap_while_running¶
test/scenarios/light/scenario_Effects_swap_while_running.json: Changing what the lights show without stopping them. A control on the running effect applies on the next frame, and a swap puts a different type in the same slot. The swap is measured on the way out, so the two effects' costs are the difference between two steps. It stops at the swap rather than swapping back, because the two tiers name the new slot differently: in-process it keeps the id the scenario gave it, and a device renames it after the type it now holds.
Mode: mutate Β· Also touches: Layer, PulseEffect
measure-pulse (measure) π¶
Setup (preceding non-measured steps):
- the-boot-effect-is-there (expect_control): A device boots on Pulse, so that is what a scenario finds unless something changed it.
- drive-it (set_control): Change a control on the running effect, which applies on the next frame.
- it-took (expect_control)
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 3,484 | β / β | β / β |
desktop-macos |
β / 5,988-1,000,000 | β / β | β / β |
desktop-windows |
β / 1,745-500,000 | β / β | β / β |
esp32s3-n16r8 |
β / 115 | β / 8284KB | β / 68KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-30desktop-windows: observed 2026-09-30esp32s3-n16r8: observed 2026-09-30
measure-rainbow (measure) π¶
The difference against the measure above is what the two effects cost relative to each other.
Setup (preceding non-measured steps):
- swap-it (replace_module): Replace the effect in its slot with another type, which is what the card's pencil does.
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 8,772 | β / β | β / β |
desktop-macos |
β / 52,632-500,000 | β / β | β / β |
desktop-windows |
β / 943-250,000 | β / β | β / β |
esp32s3-n16r8 |
β / 100 | β / 8285KB | β / 60KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-30desktop-windows: observed 2026-09-30esp32s3-n16r8: observed 2026-09-30
FileManagerModule¶
scenario_FileManager_writes_and_reads_back¶
test/scenarios/core/scenario_FileManager_writes_and_reads_back.json: A file written to the device is still there after a restart, which is the property every preset, script and configuration depends on. The one card whose whole job is the filesystem, so it is the one that proves the filesystem.
Mode: mutate Β· Also touches: FilesystemModule
Trailing setup (no measurement after)¶
write-a-file(write_file): Write through the same path the editor uses, so this exercises what a user's save does.it-is-there(expect_file): Read it straight back, which proves the write returned rather than merely reported.restart(reboot): Restart, because a write that only reached a cache is not a write.it-survived(expect_file): Still there, so the bytes reached the filesystem and came back at boot.
FirmwareUpdateModule¶
scenario_Firmware_reports_what_is_running¶
test/scenarios/core/scenario_Firmware_reports_what_is_running.json: The card that decides whether an update is offered, and therefore the one that must never guess. Its version comes from the binary rather than from a file, so a restart cannot change it and nothing persisted can edit it into something untrue. The value itself moves every release, so what is asserted is that it is the same before and after a restart rather than any particular string.
Mode: mutate Β· Also touches: SystemModule
note-the-version (measure) π¶
Read what the running binary says it is, which the update check compares against a release.
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 4,762 | β / β | β / β |
desktop-macos |
β / 5,102-31,250 | β / β | β / β |
desktop-windows |
β / 570 | β / β | β / β |
esp32s3-n16r8 |
β / 115 | β / 8291KB | β / 68KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-29desktop-windows: observed 2026-09-30esp32s3-n16r8: observed 2026-09-30
still-reporting (measure) π¶
The card answers again after the restart, which is what the update check needs it to do. What it reports is not asserted: the version moves every release and the variant differs per target.
Setup (preceding non-measured steps):
- a-version-is-reported (expect_control): An empty version is what a card reports when its source is missing, and it is the one value the update check must never compare against a release.
- restart (reboot): Restart: the version must come from the image rather than from anything persisted.
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 4,651 | β / β | β / β |
desktop-macos |
β / 8,333-33,333 | β / β | β / β |
desktop-windows |
β / 547 | β / β | β / β |
esp32s3-n16r8 |
β / 117 | β / 8325KB | β / 108KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-29desktop-windows: observed 2026-09-30esp32s3-n16r8: observed 2026-09-30
Trailing setup (no measurement after)¶
it-still-reports-one(expect_control): The same holds after the restart, since the version comes from the image rather than from anything persisted.
FluidEffect¶
scenario_Fluid_solver¶
test/scenarios/device/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 Β· live-only (skipped in-process) Β· 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-docker-arm64 |
β / 422 | β / β | β / β |
desktop-macos |
β / 28,571-34,483 | β / β | β / β |
desktop-windows |
β / 0.1 | β / β | β / β |
esp32 |
β / 57.1-57.7 | β / 33KB | β / 26KB |
esp32p4rev1-eth |
β / 130 | β / 33064KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 0.1-0.2 | β / 26174KB-32900KB | β / 224KB-296KB |
esp32s3-n16r8 |
β / 28.3-28.8 | β / 8196KB-8251KB | β / 64KB-92KB |
esp32s31 |
β / 93.5 | β / 16333KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 807 | β / β | β / β |
desktop-macos |
β / 31,250-52,632 | β / β | β / β |
desktop-windows |
β / 832 | β / β | β / β |
esp32 |
β / 118-156 | β / 27KB-33KB | β / 21KB-26KB |
esp32p4rev1-eth |
β / 416 | β / 33064KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 35.9-42.2 | β / 26251KB-32964KB | β / 224KB-296KB |
esp32s3-n16r8 |
β / 24.8-25.9 | β / 8196KB-8251KB | β / 64KB-92KB |
esp32s31 |
β / 319 | β / 16332KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 429 | β / β | β / β |
desktop-macos |
β / 8,696-14,925 | β / β | β / β |
desktop-windows |
β / 515 | β / β | β / β |
esp32 |
β / 18.7-21.8 | β / 25KB-33KB | β / 19KB-26KB |
esp32p4rev1-eth |
β / 36.9 | β / 33065KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 13.5-15.5 | β / 26250KB-32963KB | β / 224KB-296KB |
esp32s3-n16r8 |
β / 23.9-24.2 | β / 8196KB-8251KB | β / 64KB-92KB |
esp32s31 |
β / 32.3 | β / 16333KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 715 | β / β | β / β |
desktop-macos |
β / 25,000-34,483 | β / β | β / β |
desktop-windows |
β / 818 | β / β | β / β |
esp32 |
β / 57.3-60.0 | β / 31KB-34KB | β / 24KB-26KB |
esp32p4rev1-eth |
β / 132 | β / 33065KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 23.6-27.6 | β / 26246KB-32961KB | β / 224KB-288KB |
esp32s3-n16r8 |
β / 29.4-29.5 | β / 8197KB-8252KB | β / 64KB-92KB |
esp32s31 |
β / 110 | β / 16333KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 362 | β / β | β / β |
desktop-macos |
β / 8,475-15,625 | β / β | β / β |
desktop-windows |
β / 959 | β / β | β / β |
esp32 |
β / 139-835 | β / 52KB-65KB | β / 24KB-26KB |
esp32p4rev1-eth |
β / 62.6 | β / 33020KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 3.4-4.0 | β / 26161KB-32918KB | β / 220KB-296KB |
esp32s3-n16r8 |
β / 13.3-15.0 | β / 8152KB-8207KB | β / 64KB-92KB |
esp32s31 |
β / 52.8 | β / 16283KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 379 | β / β | β / β |
desktop-macos |
β / 5,714-7,752 | β / β | β / β |
desktop-windows |
β / 425 | β / β | β / β |
esp32 |
β / 7,874-9,346 | β / 52KB-55KB | β / 12KB-26KB |
esp32p4rev1-eth |
β / 27.3 | β / 32930KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 2.3-4.1 | β / 26111KB-32828KB | β / 224KB-296KB |
esp32s3-n16r8 |
β / 12.1-14.0 | β / 8062KB-8117KB | β / 64KB-92KB |
esp32s31 |
β / 21.8 | β / 16192KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 378 | β / β | β / β |
desktop-macos |
β / 6,135-7,692 | β / β | β / β |
desktop-windows |
β / 318 | β / β | β / β |
esp32 |
β / 6,757-7,692 | β / 53KB-55KB | β / 12KB-26KB |
esp32p4rev1-eth |
β / 23.0 | β / 32930KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 6.2-7.0 | β / 26114KB-32828KB | β / 224KB-296KB |
esp32s3-n16r8 |
β / 13.4-13.7 | β / 8062KB-8117KB | β / 64KB-92KB |
esp32s31 |
β / 25.2 | β / 16192KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 341 | β / β | β / β |
desktop-macos |
β / 5,435-7,752 | β / β | β / β |
desktop-windows |
β / 236 | β / β | β / β |
esp32 |
β / 8,333-9,434 | β / 53KB-55KB | β / 12KB-26KB |
esp32p4rev1-eth |
β / 27.7 | β / 32930KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 2.9-3.3 | β / 26097KB-32828KB | β / 220KB-296KB |
esp32s3-n16r8 |
β / 13.3-13.6 | β / 8062KB-8117KB | β / 64KB-92KB |
esp32s31 |
β / 24.8 | β / 16192KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 865 | β / β | β / β |
desktop-macos |
β / 21,277-28,571 | β / β | β / β |
desktop-windows |
β / 436 | β / β | β / β |
esp32 |
β / 54.0-54.1 | β / 21KB-23KB | β / 8KB |
esp32p4rev1-eth |
β / 104 | β / 33054KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 2.1-3.3 | β / 25557KB-32946KB | β / 152KB-288KB |
esp32s3-n16r8 |
β / 19.7-21.4 | β / 8186KB-8241KB | β / 64KB-92KB |
esp32s31 |
β / 74.6 | β / 16322KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
cube-20-h (set_control) π¶
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 1,171 | β / β | β / β |
desktop-macos |
β / 71,429-90,909 | β / β | β / β |
desktop-windows |
β / 1.6 | β / β | β / β |
esp32 |
β / 2,155-2,315 | β / 75KB-77KB | β / 24KB-26KB |
esp32p4rev1-eth |
β / 85.5 | β / 33033KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 0.3-1.5 | β / 26228KB-32989KB | β / 216KB-288KB |
esp32s3-n16r8 |
β / 73.4-74.0 | β / 8224KB-8279KB | β / 64KB-92KB |
esp32s31 |
β / 547 | β / 16361KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
cube-20-d (set_control) π¶
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 297 | β / β | β / β |
desktop-macos |
β / 3,623-4,587 | β / β | β / β |
desktop-windows |
β / 567 | β / β | β / β |
esp32 |
β / 8,197-10,309 | β / 61KB-63KB | β / 20KB-28KB |
esp32p4rev1-eth |
β / 14.1 | β / 32865KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 0.5-1.3 | β / 25998KB-32763KB | β / 212KB-296KB |
esp32s3-n16r8 |
β / 9.7-9.8 | β / 7985KB-8040KB | β / 64KB-92KB |
esp32s31 |
β / 15.0 | β / 16138KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 1,168 | β / β | β / β |
desktop-macos |
β / 83,333-90,909 | β / β | β / β |
desktop-windows |
β / 26.5 | β / β | β / β |
esp32 |
β / 177 | β / 59KB-61KB | β / 46KB-52KB |
esp32p4rev1-eth |
β / 87.4 | β / 33022KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 1.8 | β / 26226KB | β / 216KB |
esp32s3-n16r8 |
β / 72.0-72.1 | β / 8201KB-8256KB | β / 64KB-92KB |
esp32s31 |
β / 486 | β / 16361KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 1,422 | β / β | β / β |
desktop-macos |
β / 111,111-125,000 | β / β | β / β |
desktop-windows |
β / 1.5 | β / β | β / β |
esp32 |
β / 213-217 | β / 63KB-65KB | β / 46KB-52KB |
esp32p4rev1-eth |
β / 107 | β / 33037KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 3.6 | β / 26222KB | β / 216KB |
esp32s3-n16r8 |
β / 80.6-88.7 | β / 8205KB-8260KB | β / 64KB-92KB |
esp32s31 |
β / 703 | β / 16364KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
grid-16-h (set_control) π¶
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 1,727 | β / β | β / β |
desktop-macos |
β / 142,857 | β / β | β / β |
desktop-windows |
β / 6.6 | β / β | β / β |
esp32 |
β / 482-485 | β / 66KB-68KB | β / 46KB-52KB |
esp32p4rev1-eth |
β / 131 | β / 33048KB | β / 252KB |
esp32p4rev1-eth-wifi |
β / 1.3 | β / 26285KB | β / 224KB |
esp32s3-n16r8 |
β / 88.9-112 | β / 8208KB-8262KB | β / 64KB-92KB |
esp32s31 |
β / 870 | β / 16367KB | β / 90KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
GridLayout¶
scenario_GridLayout_resize¶
test/scenarios/device/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 Β· live-only (skipped in-process) Β· 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-docker-arm64 |
β / 532 | β / β | β / β |
desktop-macos |
β₯ 8,333 / 6,452-8,333 | unlimited / β | β / β |
desktop-windows |
β / 380-4,566 | β / β | β / β |
esp32 |
β / 124-740 | β / 57KB-83KB | β / 22KB-48KB |
esp32-eth |
β / 10.8 | β / 132KB | β / 48KB |
esp32-eth-wifi |
β₯ 10.0 / 12.4 | β₯ 103KB / 93KB | β / 48KB |
esp32p4rev1-eth |
β / 129-139 | β / 32818KB-33206KB | β / 280KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.1 | β / 32632KB | β / 248KB |
esp32s3-n16r8 |
β / 51.0-54.3 | β / 7984KB-8293KB | β / 76KB-100KB |
esp32s31 |
β / 112 | β / 16180KB | β / 122KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32-eth: observed 2026-06-02esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 652 | β / β | β / β |
desktop-macos |
β₯ 16,667 / 13,158-15,625 | unlimited / β | β / β |
desktop-windows |
β / 541-10,638 | β / β | β / β |
esp32 |
β / 337-1,745 | β / 63KB-64KB | β / 17KB-22KB |
esp32-eth |
β / 26.5 | β / 114KB | β / 48KB |
esp32-eth-wifi |
β₯ 22.2 / 31.8 | β₯ 83KB / 75KB | β / 24KB |
esp32p4rev1-eth |
β / 360-443 | β / 32885KB-33214KB | β / 272KB-376KB |
esp32p4rev1-eth-wifi |
β / 1.7 | β / 32780KB | β / 248KB |
esp32s3-n16r8 |
β / 141-142 | β / 8054KB-8310KB | β / 84KB-100KB |
esp32s31 |
β / 301 | β / 16273KB | β / 140KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32-eth: observed 2026-06-02esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 499 | β / β | β / β |
desktop-macos |
β₯ 8,333 / 6,993-8,333 | unlimited / β | β / β |
desktop-windows |
β / 416-4,608 | β / β | β / β |
esp32 |
β / 167-839 | β / 62KB-83KB | β / 24KB-52KB |
esp32-eth |
β / 10.4 | β / 132KB | β / 48KB |
esp32-eth-wifi |
β₯ 10.0 / 12.2 | β₯ 103KB / 93KB | β / 52KB |
esp32p4rev1-eth |
β / 125-165 | β / 32817KB-33206KB | β / 280KB-376KB |
esp32p4rev1-eth-wifi |
β / 2.5 | β / 32710KB | β / 248KB |
esp32s3-n16r8 |
β / 58.7-63.8 | β / 7984KB-8292KB | β / 76KB-100KB |
esp32s31 |
β / 124 | β / 16182KB | β / 124KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: contract set 2026-06-02 "initial contract" Β· observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32-eth: observed 2026-06-02esp32-eth-wifi: contract set 2026-06-02 "initial contract" Β· observed 2026-06-02esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
scenario_Layouts_resize_reallocates_live¶
test/scenarios/light/scenario_Layouts_resize_reallocates_live.json: Changing where the lights are, while they are running. Every control applies on the next frame, so a resize reallocates the buffer under a rendering pipeline rather than after a reboot.
Mode: mutate Β· Also touches: Layouts, Layer
measure-small (measure) π¶
Setup (preceding non-measured steps):
- small (set_control): A 16x16 panel, the size a device boots with.
- small-height (set_control)
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 5,155 | β / β | β / β |
desktop-macos |
β / 18,868-90,909 | β / β | β / β |
desktop-windows |
β / 511-10,753 | β / β | β / β |
esp32s3-n16r8 |
β / 98.9 | β / 8284KB | β / 68KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-30desktop-windows: observed 2026-09-30esp32s3-n16r8: observed 2026-09-30
measure-grown (measure) π¶
Setup (preceding non-measured steps):
- grow (set_control): Four times the lights, which is where a reallocation would fail if one were going to.
- grow-height (set_control)
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 1,727 | β / β | β / β |
desktop-macos |
β / 21,277-83,333 | β / β | β / β |
desktop-windows |
β / 375-28,571 | β / β | β / β |
esp32s3-n16r8 |
β / 30.9 | β / 8273KB | β / 64KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-30desktop-windows: observed 2026-09-30esp32s3-n16r8: observed 2026-09-30
measure-shrunk (measure) π¶
Setup (preceding non-measured steps):
- it-grew (expect_control)
- shrink-back (set_control): Down again, because releasing memory is the half that leaks when it is wrong.
- shrink-height (set_control)
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 5,025 | β / β | β / β |
desktop-macos |
β / 19,608-111,111 | β / β | β / β |
desktop-windows |
β / 459-76,923 | β / β | β / β |
esp32s3-n16r8 |
β / 116 | β / 8278KB | β / 64KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-30desktop-windows: observed 2026-09-30esp32s3-n16r8: observed 2026-09-30
Trailing setup (no measurement after)¶
restart(reboot): Restart: a layout that does not survive one is entered again on every power cut.the-size-survived(expect_control)
Layer¶
scenario_perf_full¶
test/scenarios/device/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 Β· live-only (skipped in-process) Β· 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-docker-arm64 |
β / 5,882 | β / β | β / β |
desktop-macos |
β / 333,333-1,000,000 | β / β | β / β |
desktop-windows |
β / 514-333,333 | β / β | β / β |
esp32 |
β / 758-2,865 | β / 74KB-108KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 6,369-17,544 | β / 33175KB-33226KB | β / 280KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.2 | β / 33008KB | β / 288KB |
esp32s3-n16r8 |
β / 3,937-4,032 | β / 8323KB-8331KB | β / 80KB-96KB |
esp32s31 |
β / 4,219 | β / 16398KB | β / 136KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 7,519 | β / β | β / β |
desktop-macos |
β / 333,333-1,000,000 | β / β | β / β |
desktop-windows |
β / 505-333,333 | β / β | β / β |
esp32 |
β / 1,142-4,525 | β / 81KB-118KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 7,692-18,868 | β / 33175KB-33228KB | β / 280KB-376KB |
esp32p4rev1-eth-wifi |
β / 4.4 | β / 32942KB | β / 288KB |
esp32s3-n16r8 |
β / 5,128-5,319 | β / 8330KB-8331KB | β / 84KB-96KB |
esp32s31 |
β / 5,236 | β / 16398KB | β / 136KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 9,259 | β / β | β / β |
desktop-macos |
β / 500,000-1,000,000 | β / β | β / β |
desktop-windows |
β / 578-333,333 | β / β | β / β |
esp32 |
β / 1,117-4,545 | β / 80KB-118KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 8,130-18,519 | β / 33177KB-33226KB | β / 280KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.4 | β / 33006KB | β / 288KB |
esp32s3-n16r8 |
β / 5,076-5,319 | β / 8331KB-8333KB | β / 84KB-96KB |
esp32s31 |
β / 4,292 | β / 16399KB | β / 136KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 7,874 | β / β | β / β |
desktop-macos |
β / 250,000-1,000,000 | β / β | β / β |
desktop-windows |
β / 601-1,000,000 | β / β | β / β |
esp32 |
β / 2,336-5,525 | β / 82KB-119KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 10,526-10,638 | β / 33174KB-33224KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 3.7 | β / 33004KB | β / 288KB |
esp32s3-n16r8 |
β / 5,650-6,135 | β / 8330KB-8331KB | β / 84KB-104KB |
esp32s31 |
β / 6,452 | β / 16399KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-23desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 7,692 | β / β | β / β |
desktop-macos |
β / 1,000,000 | β / β | β / β |
desktop-windows |
β / 647-333,333 | β / β | β / β |
esp32 |
β / 1,080-4,292 | β / 81KB-119KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 7,812-18,182 | β / 33174KB-33228KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 1,862 | β / 33005KB | β / 288KB |
esp32s3-n16r8 |
β / 5,128-5,181 | β / 8330KB | β / 84KB-104KB |
esp32s31 |
β / 4,444 | β / 16399KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 7,353 | β / β | β / β |
desktop-macos |
β / 1,000,000 | β / β | β / β |
desktop-windows |
β / 561-333,333 | β / β | β / β |
esp32 |
β / 1,021-3,311 | β / 77KB-117KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 7,463-17,544 | β / 33172KB-33226KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 1,063 | β / 33002KB | β / 288KB |
esp32s3-n16r8 |
β / 4,975-5,051 | β / 8328KB-8331KB | β / 84KB-104KB |
esp32s31 |
β / 5,102 | β / 16397KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 7,519 | β / β | β / β |
desktop-macos |
β / 1,000,000 | β / β | β / β |
desktop-windows |
β / 89.3-333,333 | β / β | β / β |
esp32 |
β / 403-1,362 | β / 68KB-91KB | β / 38KB-68KB |
esp32p4rev1-eth |
β / 383-17,857 | β / 33170KB-33200KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.2 | β / 33001KB | β / 288KB |
esp32s3-n16r8 |
β / 412-413 | β / 8307KB-8326KB | β / 84KB-96KB |
esp32s31 |
β / 421 | β / 16394KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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 on the IDF-backed peripheral, capped to 64 LEDs on lane 0 (it needs all 8 data pins; unused lanes get 0 LEDs). S3, P4 and S31 only, a classic ESP32 naming the same backend I2S-IDF.
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 7,042 | β / β | β / β |
desktop-macos |
β / 1,000,000 | β / β | β / β |
desktop-windows |
β / 506-333,333 | β / β | β / β |
esp32 |
β / 3,876-3,937 | β / 68KB-71KB | β / 32KB-34KB |
esp32p4rev1-eth |
β / 459 | β / 33158KB | β / 288KB |
esp32p4rev1-eth-wifi |
β / 0.2 | β / 32990KB | β / 288KB |
esp32s3-n16r8 |
β / 381-452 | β / 8314KB-8330KB | β / 84KB-104KB |
esp32s31 |
β / 459 | β / 16382KB | β / 136KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 9,259 | β / β | β / β |
desktop-macos |
β / 1,000,000 | β / β | β / β |
desktop-windows |
β / 629-333,333 | β / β | β / β |
esp32 |
β / 1,115-1,389 | β / 79KB-92KB | β / 38KB-46KB |
esp32p4rev1-eth |
β / 454-17,857 | β / 33158KB-33225KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.2 | β / 32990KB | β / 288KB |
esp32s3-n16r8 |
β / 405-461 | β / 8130KB-8314KB | β / 80KB-100KB |
esp32s31 |
β / 460 | β / 16382KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 9,259 | β / β | β / β |
desktop-macos |
β / 1,000,000 | β / β | β / β |
desktop-windows |
β / 493-333,333 | β / β | β / β |
esp32 |
β / 1,218-4,292 | β / 82KB-115KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 8,130-18,868 | β / 33174KB-33226KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.6 | β / 33005KB | β / 288KB |
esp32s3-n16r8 |
β / 5,319-5,348 | β / 8330KB-8333KB | β / 84KB-108KB |
esp32s31 |
β / 4,405 | β / 16398KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 4,049 | β / β | β / β |
desktop-macos |
β / 200,000-250,000 | β / β | β / β |
desktop-windows |
β / 520-83,333 | β / β | β / β |
esp32 |
β / 309-1,433 | β / 78KB-114KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 2,358-7,576 | β / 33168KB-33225KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.6 | β / 32999KB | β / 288KB |
esp32s3-n16r8 |
β / 1,647-1,733 | β / 8322KB-8323KB | β / 84KB-108KB |
esp32s31 |
β / 2,183 | β / 16394KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 1,269 | β / β | β / β |
desktop-macos |
β / 50,000-55,556 | β / β | β / β |
desktop-windows |
β / 544-20,408 | β / β | β / β |
esp32 |
β / 67.2-388 | β / 58KB-97KB | β / 34KB-64KB |
esp32p4rev1-eth |
β / 795-2,232 | β / 33139KB-33218KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.6 | β / 32975KB | β / 288KB |
esp32s3-n16r8 |
β / 290-361 | β / 8290KB-8296KB | β / 80KB-108KB |
esp32s31 |
β / 671 | β / 16369KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 503 | β / β | β / β |
desktop-macos |
β / 10,101-13,889 | β / β | β / β |
desktop-windows |
β / 337-5,102 | β / β | β / β |
esp32 |
β / 91.8-473 | β / 81KB-90KB | β / 34KB-38KB |
esp32p4rev1-eth |
β / 165-573 | β / 33007KB-33182KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 13.4 | β / 32879KB | β / 288KB |
esp32s3-n16r8 |
β / 68.7-72.9 | β / 8158KB-8283KB | β / 80KB-108KB |
esp32s31 |
β / 147 | β / 16273KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 6,757 | β / β | β / β |
desktop-macos |
β / 200,000-250,000 | β / β | β / β |
desktop-windows |
β / 517-111,111 | β / β | β / β |
esp32 |
β / 1,623-2,545 | β / 82KB-120KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 3,367-4,673 | β / 33133KB-33229KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.3 | β / 33006KB | β / 288KB |
esp32s3-n16r8 |
β / 2,451-2,710 | β / 8284KB-8329KB | β / 84KB-108KB |
esp32s31 |
β / 3,745 | β / 16400KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 2,463 | β / β | β / β |
desktop-macos |
β / 55,556-62,500 | β / β | β / β |
desktop-windows |
β / 454-27,778 | β / β | β / β |
esp32 |
β / 584-743 | β / 76KB-113KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 898-1,215 | β / 33129KB-33227KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.8 | β / 33001KB | β / 288KB |
esp32s3-n16r8 |
β / 635-751 | β / 8280KB-8331KB | β / 84KB-108KB |
esp32s31 |
β / 1,221 | β / 16395KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 693 | β / β | β / β |
desktop-macos |
β / 14,706-15,625 | β / β | β / β |
desktop-windows |
β / 415-6,897 | β / β | β / β |
esp32 |
β / 172-226 | β / 60KB-90KB | β / 25KB-64KB |
esp32p4rev1-eth |
β / 229-358 | β / 33111KB-33218KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 1.9 | β / 32983KB | β / 288KB |
esp32s3-n16r8 |
β / 149-189 | β / 8262KB-8320KB | β / 84KB-108KB |
esp32s31 |
β / 330 | β / 16377KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 410 | β / β | β / β |
desktop-macos |
β / 2,950-3,968 | β / β | β / β |
desktop-windows |
β / 243-1,689 | β / β | β / β |
esp32 |
β / 171-219 | β / 74KB-84KB | β / 34KB-38KB |
esp32p4rev1-eth |
β / 57.4-85.4 | β / 33039KB-33182KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 1.8 | β / 32911KB | β / 288KB |
esp32s3-n16r8 |
β / 32.5-39.5 | β / 8190KB-8273KB | β / 76KB-108KB |
esp32s31 |
β / 74.8 | β / 16305KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 9,174 | β / β | β / β |
desktop-macos |
β / 500,000-1,000,000 | β / β | β / β |
desktop-windows |
β / 569-500,000 | β / β | β / β |
esp32 |
β / 2,874-4,167 | β / 82KB-116KB | β / 34KB-92KB |
esp32p4rev1-eth |
β / 6,494-8,772 | β / 33022KB-33224KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.2 | β / 32895KB | β / 288KB |
esp32s3-n16r8 |
β / 4,202-4,525 | β / 8173KB-8328KB | β / 84KB-100KB |
esp32s31 |
β / 5,236 | β / 16399KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 4,115 | β / β | β / β |
desktop-macos |
β / 200,000-250,000 | β / β | β / β |
desktop-windows |
β / 518-111,111 | β / β | β / β |
esp32 |
β / 1,227-1,425 | β / 76KB-101KB | β / 34KB-92KB |
esp32p4rev1-eth |
β / 1,876-3,155 | β / 33015KB-33218KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.6 | β / 32887KB | β / 288KB |
esp32s3-n16r8 |
β / 1,443-1,709 | β / 8166KB-8325KB | β / 84KB-100KB |
esp32s31 |
β / 2,532 | β / 16392KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 1,131 | β / β | β / β |
desktop-macos |
β / 47,619-66,667 | β / β | β / β |
desktop-windows |
β / 1,712-27,778 | β / β | β / β |
esp32 |
β / 332-397 | β / 57KB-95KB | β / 25KB-64KB |
esp32p4rev1-eth |
β / 486-894 | β / 32989KB-33194KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.6 | β / 32862KB | β / 288KB |
esp32s3-n16r8 |
β / 268-334 | β / 8140KB-8297KB | β / 84KB-100KB |
esp32s31 |
β / 745 | β / 16364KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 498 | β / β | β / β |
desktop-macos |
β / 13,889-15,873 | β / β | β / β |
desktop-windows |
β / 430-6,897 | β / β | β / β |
esp32 |
β / 170-188 | β / 73KB-80KB | β / 34KB |
esp32p4rev1-eth |
β / 102-176 | β / 32884KB-33089KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.3 | β / 32756KB | β / 288KB |
esp32s3-n16r8 |
β / 64.2-67.3 | β / 8035KB-8180KB | β / 76KB-100KB |
esp32s31 |
β / 161 | β / 16260KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
scenario_perf_light¶
test/scenarios/device/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 Β· live-only (skipped in-process) Β· 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-docker-arm64 |
β / 3,049 | β / β | β / β |
desktop-macos |
β / 1,000,000 | β / β | β / β |
desktop-windows |
β / 583-333,333 | β / β | β / β |
esp32 |
β / 816-2,604 | β / 82KB-119KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 2,732-5,682 | β / 33161KB-33228KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.2 | β / 32959KB-32983KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 3,861-5,319 | β / 8266KB-8316KB | β / 80KB-100KB |
esp32s31 |
β / 3,584 | β / 16510KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 8,130 | β / β | β / β |
desktop-macos |
β / 250,000-1,000,000 | β / β | β / β |
desktop-windows |
β / 588-1,000,000 | β / β | β / β |
esp32 |
β / 2,188-5,556 | β / 81KB-115KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 601-10,309 | β / 33148KB-33226KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 1.3-1.8 | β / 32957KB-32980KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 5,952-6,803 | β / 8265KB-8330KB | β / 80KB-108KB |
esp32s31 |
β / 6,250 | β / 16508KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-03desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 10,204 | β / β | β / β |
desktop-macos |
β / 500,000-1,000,000 | β / β | β / β |
desktop-windows |
β / 598-1,000,000 | β / β | β / β |
esp32 |
β / 2,809-5,780 | β / 83KB-117KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 595-10,753 | β / 33148KB-33226KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 26.9-410 | β / 33008KB-33043KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 7,092-7,143 | β / 8265KB-8328KB | β / 80KB-108KB |
esp32s31 |
β / 6,494 | β / 16508KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-05desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 7,519 | β / β | β / β |
desktop-macos |
β / 1,000,000 | β / β | β / β |
desktop-windows |
β / 486-500,000 | β / β | β / β |
esp32 |
β / 3,759-4,255 | β / 83KB-117KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 121-6,494 | β / 33150KB-33224KB | β / 288KB-376KB |
esp32p4rev1-eth-wifi |
β / 1.3-163 | β / 32980KB-33034KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 5,376-5,435 | β / 8267KB-8332KB | β / 80KB-108KB |
esp32s31 |
β / 6,329 | β / 16509KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 3,774 | β / β | β / β |
desktop-macos |
β / 200,000-250,000 | β / β | β / β |
desktop-windows |
β / 828-111,111 | β / β | β / β |
esp32 |
β / 1,266-1,464 | β / 78KB-112KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 421-1,880 | β / 33136KB-33221KB | β / 280KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.4-0.5 | β / 32995KB-33028KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 1,541-1,761 | β / 8260KB-8319KB | β / 80KB-108KB |
esp32s31 |
β / 2,604 | β / 16498KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 1,282 | β / β | β / β |
desktop-macos |
β / 47,619-62,500 | β / β | β / β |
desktop-windows |
β / 571-28,571 | β / β | β / β |
esp32 |
β / 272-398 | β / 58KB-94KB | β / 38KB-72KB |
esp32p4rev1-eth |
β / 491-895 | β / 33080KB-33195KB | β / 296KB-376KB |
esp32p4rev1-eth-wifi |
β / 0.2-1.4 | β / 32950KB-32972KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 317-329 | β / 8234KB-8277KB | β / 80KB-108KB |
esp32s31 |
β / 726 | β / 16450KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
MoonCloudModule¶
scenario_MoonCloud_consent_is_off_until_asked¶
test/scenarios/core/scenario_MoonCloud_consent_is_off_until_asked.json: Nothing leaves the device until someone says so. Both MoonCloud children carry their own consent, because wanting a joint light show is not agreeing to usage reporting, and each is proven off by default and durable once set.
Mode: mutate Β· Also touches: MoonStatsModule, MoonTalkModule
Trailing setup (no measurement after)¶
stats-consent-is-off(expect_control): A fresh device reports nothing. This is the assertion that matters most on this card.talk-consent-is-off(expect_control): And the same for Talk, which is a separate decision rather than the same one.opt-in(set_control): Someone switches it on, which is the only way it ever becomes true.talk-is-still-off(expect_control)restart(reboot): Restart: a consent that quietly reverts would report without asking twice.consent-survived(expect_control)opt-out-again(set_control): And off again, because withdrawing has to work as well as granting.it-is-off(expect_control)
NetworkModule¶
scenario_Network_identity_and_discovery¶
test/scenarios/core/scenario_Network_identity_and_discovery.json: The network card's own settings, and the one thing a rename could silently change: how this device is advertised and how it types the peers it finds. mDNS and the device list both key on names, so each is set, restarted, and read back rather than merely written.
Mode: mutate Β· Also touches: DevicesModule, SystemModule
Trailing setup (no measurement after)¶
mdns-off(set_control): mDNS is what lets a browser reach the device by name rather than by address.mdns-is-off(expect_control)mdns-on(set_control): Back on, because a device nobody can find by name is a support call.wled-compatible-off(set_control): The WLED compatibility shim is what makes this device appear in the WLED apps.shim-is-off(expect_control)wled-compatible-on(set_control)restart(reboot): Restart, because a network setting that does not survive one is a setting a user has to enter twice.mdns-survived(expect_control)shim-survived(expect_control)
ParallelLedDriver¶
scenario_peripheral_grid_sweep¶
test/scenarios/device/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 Β· live-only (skipped in-process) Β· Also touches: I80Peripheral, MoonI80Peripheral, ParlioPeripheral, 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-docker-arm64 |
β / 2,439 | β / β | β / β |
desktop-macos |
β / 200,000-250,000 | β / β | β / β |
desktop-windows |
β / 94.1-111,111 | β / β | β / β |
esp32 |
β / 98.8-2,273 | β / 56KB-73KB | β / 38KB-54KB |
esp32p4rev1-eth |
β / 7.1-125 | β / 33113KB-33143KB | β / 296KB-344KB |
esp32p4rev1-eth-wifi |
β / 5.2-13.6 | β / 33054KB-33059KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 123 | β / 8143KB-8169KB | β / 80KB-100KB |
esp32s31 |
β / 126-5,376 | β / 16395KB-16573KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 2,309 | β / β | β / β |
desktop-macos |
β / 52,632-62,500 | β / β | β / β |
desktop-windows |
β / 94.1-27,027 | β / β | β / β |
esp32 |
β / 26.6-670 | β / 74KB-76KB | β / 38KB-48KB |
esp32p4rev1-eth |
β / 33.0-2,037 | β / 33067KB-33074KB | β / 280KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.5-7.1 | β / 33046KB-33052KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 32.4 | β / 8136KB-8163KB | β / 76KB-100KB |
esp32s31 |
β / 34.0-1,473 | β / 16388KB-16500KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 690 | β / β | β / β |
desktop-macos |
β / 13,514-15,385 | β / β | β / β |
desktop-windows |
β / 61.6-6,250 | β / β | β / β |
esp32 |
β / 14.2-179 | β / 59KB-65KB | β / 25KB-38KB |
esp32p4rev1-eth |
β / 16.7-453 | β / 32962KB-33089KB | β / 280KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.6 | β / 33019KB-33024KB | β / 288KB |
esp32s3-n16r8 |
β / 13.5-15.1 | β / 8116KB-8142KB | β / 72KB-100KB |
esp32s31 |
β / 17.3-346 | β / 16194KB-16368KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 409 | β / β | β / β |
desktop-macos |
β / 3,077-3,891 | β / β | β / β |
desktop-windows |
β / 70.7-1,541 | β / β | β / β |
esp32 |
β / 13.7-719 | β / 88KB | β / 38KB |
esp32p4rev1-eth |
β / 12.9-87.0 | β / 32880KB-32890KB | β / 288KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.4-0.6 | β / 32914KB-32916KB | β / 288KB |
esp32s3-n16r8 |
β / 11.7-14.2 | β / 8044KB-8071KB | β / 72KB-100KB |
esp32s31 |
β / 15.5-162 | β / 14966KB-16296KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
measure-i80-16 (measure) π¶
Setup (preceding non-measured steps):
- remove-rmt (remove_module)
- add-i80 (add_module): The IDF-backed peripheral on a chip whose esp_lcd backend is LCD_CAM (S3, P4, S31); a classic ESP32 names it I2S-IDF and skips here. 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-docker-arm64 |
β / 6,536 | β / β | β / β |
desktop-macos |
β / 200,000-250,000 | β / β | β / β |
desktop-windows |
β / 584-111,111 | β / β | β / β |
esp32 |
β / 1,543-1,672 | β / 70KB-73KB | β / 34KB-44KB |
esp32p4rev1-eth |
β / 421-1,305 | β / 32980KB-33166KB | β / 280KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.4-3.1 | β / 32993KB-33006KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 1,274-1,302 | β / 8135KB-8161KB | β / 80KB-100KB |
esp32s31 |
β / 1,299-2,469 | β / 16387KB-16599KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 2,421 | β / β | β / β |
desktop-macos |
β / 52,632-62,500 | β / β | β / β |
desktop-windows |
β / 485-27,027 | β / β | β / β |
esp32 |
β / 431-501 | β / 62KB-65KB | β / 34KB-44KB |
esp32p4rev1-eth |
β / 48.9-484 | β / 33148KB-33161KB | β / 296KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.2-5.2 | β / 32957KB-33007KB | β / 272KB-296KB |
esp32s3-n16r8 |
β / 390-463 | β / 8117KB-8143KB | β / 80KB-100KB |
esp32s31 |
β / 486-1,835 | β / 16369KB-16594KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 683 | β / β | β / β |
desktop-macos |
β / 12,346-15,873 | β / β | β / β |
desktop-windows |
β / 442-6,579 | β / β | β / β |
esp32 |
β / 128-174 | β / 35KB-53KB | β / 8KB-12KB |
esp32p4rev1-eth |
β / 266-536 | β / 33143KB-33147KB | β / 296KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.8-1.8 | β / 32989KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 111-113 | β / 8045KB-8071KB | β / 80KB-100KB |
esp32s31 |
β / 133-439 | β / 16297KB-16576KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 388 | β / β | β / β |
desktop-macos |
β / 2,660-3,953 | β / β | β / β |
desktop-windows |
β / 238-1,553 | β / β | β / β |
esp32 |
β / 178-649 | β / 67KB-79KB | β / 32KB-34KB |
esp32p4rev1-eth |
β / 111-266 | β / 33067KB-33071KB | β / 280KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.6-1.7 | β / 32917KB-32918KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 23.0-27.5 | β / 7757KB-7783KB | β / 80KB-100KB |
esp32s31 |
β / 29.5-151 | β / 16009KB-16504KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 3,802 | β / β | β / β |
desktop-macos |
β / 142,857-250,000 | β / β | β / β |
desktop-windows |
β / 564-111,111 | β / β | β / β |
esp32 |
β / 1,272-2,364 | β / 77KB-80KB | β / 34KB-38KB |
esp32p4rev1-eth |
β / 206-1,034 | β / 33121KB-33166KB | β / 288KB-344KB |
esp32p4rev1-eth-wifi |
β / 4.6-4.9 | β / 32999KB-33001KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 1,175-1,212 | β / 8135KB-8161KB | β / 80KB-100KB |
esp32s31 |
β / 1,300-4,167 | β / 16387KB-16599KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 2,278 | β / β | β / β |
desktop-macos |
β / 58,824-62,500 | β / β | β / β |
desktop-windows |
β / 566-27,027 | β / β | β / β |
esp32 |
β / 472-645 | β / 73KB-75KB | β / 34KB-38KB |
esp32p4rev1-eth |
β / 266-412 | β / 33091KB-33161KB | β / 272KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.6 | β / 33006KB-33007KB | β / 296KB |
esp32s3-n16r8 |
β / 407-464 | β / 8117KB-8143KB | β / 80KB-100KB |
esp32s31 |
β / 487-1,477 | β / 16369KB-16595KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 679 | β / β | β / β |
desktop-macos |
β / 12,658-15,625 | β / β | β / β |
desktop-windows |
β / 503-6,452 | β / β | β / β |
esp32 |
β / 148-173 | β / 54KB-58KB | β / 22KB-38KB |
esp32p4rev1-eth |
β / 25.4-266 | β / 33028KB-33143KB | β / 288KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.6 | β / 32988KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 114-116 | β / 8045KB-8071KB | β / 80KB-100KB |
esp32s31 |
β / 142-653 | β / 16297KB-16577KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 398 | β / β | β / β |
desktop-macos |
β / 3,067-3,953 | β / β | β / β |
desktop-windows |
β / 218-1,560 | β / β | β / β |
esp32 |
β / 180-662 | β / 79KB-87KB | β / 38KB |
esp32p4rev1-eth |
β / 4.5-150 | β / 33071KB-33124KB | β / 296KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.2-1.3 | β / 32865KB-32917KB | β / 280KB-296KB |
esp32s3-n16r8 |
β / 28.6-28.9 | β / 7757KB-7783KB | β / 80KB-100KB |
esp32s31 |
β / 35.4-165 | β / 16009KB-16499KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 6,494 | β / β | β / β |
desktop-macos |
β / 166,667-250,000 | β / β | β / β |
desktop-windows |
β / 507-111,111 | β / β | β / β |
esp32 |
β / 1,215-2,342 | β / 79KB-80KB | β / 34KB-38KB |
esp32p4rev1-eth |
β / 250-2,584 | β / 33119KB-33166KB | β / 288KB-344KB |
esp32p4rev1-eth-wifi |
β / 3.1-4.8 | β / 32999KB-33001KB | β / 296KB |
esp32s3-n16r8 |
β / 1,054-1,209 | β / 8135KB-8161KB | β / 80KB-100KB |
esp32s31 |
β / 1,295-2,703 | β / 16387KB-16595KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 2,392 | β / β | β / β |
desktop-macos |
β / 40,000-62,500 | β / β | β / β |
desktop-windows |
β / 664-27,027 | β / β | β / β |
esp32 |
β / 487-540 | β / 73KB-76KB | β / 34KB-38KB |
esp32p4rev1-eth |
β / 266-1,961 | β / 33119KB-33161KB | β / 288KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.4-0.6 | β / 33006KB | β / 296KB |
esp32s3-n16r8 |
β / 480-483 | β / 8113KB-8143KB | β / 80KB-100KB |
esp32s31 |
β / 487-1,745 | β / 16369KB-16595KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 675 | β / β | β / β |
desktop-macos |
β / 11,236-15,873 | β / β | β / β |
desktop-windows |
β / 629-6,289 | β / β | β / β |
esp32 |
β / 167-175 | β / 53KB-58KB | β / 22KB-38KB |
esp32p4rev1-eth |
β / 266-432 | β / 33118KB-33143KB | β / 288KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.6-0.8 | β / 32988KB-32989KB | β / 296KB |
esp32s3-n16r8 |
β / 116 | β / 8045KB-8071KB | β / 80KB-100KB |
esp32s31 |
β / 141-393 | β / 16297KB-16577KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 561 | β / β | β / β |
desktop-macos |
β / 2,967-3,937 | β / β | β / β |
desktop-windows |
β / 233-1,560 | β / β | β / β |
esp32 |
β / 201-540 | β / 77KB-89KB | β / 34KB-38KB |
esp32p4rev1-eth |
β / 216-266 | β / 33071KB-33119KB | β / 288KB-344KB |
esp32p4rev1-eth-wifi |
β / 0.2-0.6 | β / 32916KB | β / 288KB-296KB |
esp32s3-n16r8 |
β / 28.9-29.1 | β / 7757KB-7783KB | β / 80KB-100KB |
esp32s31 |
β / 36.2-81.5 | β / 16009KB-16504KB | β / 136KB-164KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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/device/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 Β· live-only (skipped in-process) Β· Also touches: I80Peripheral, MoonI80Peripheral, ParlioPeripheral, 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 IDF-backed peripheral, named LCD-IDF on a chip whose esp_lcd backend is LCD_CAM (S3, P4, S31). A classic ESP32 names the same backend I2S-IDF and skips this ladder, since one unavailable value takes the module out of the run: covering classic needs its own scenario rather than a step here.
Performance (contract / observed), tick stored, FPS shown:
| Board | FPS | heap | block |
|---|---|---|---|
desktop-docker-arm64 |
β / 2,933 | β / β | β / β |
desktop-macos |
β / 200,000-250,000 | β / β | β / β |
desktop-windows |
β / 541-111,111 | β / β | β / β |
esp32 |
β / 3,401-5,155 | β / 72KB-74KB | β / 34KB-54KB |
esp32p4rev1-eth |
β / 484-4,808 | β / 33142KB-33166KB | β / 296KB-344KB |
esp32p4rev1-eth-wifi |
β / 205 | β / 32884KB | β / 248KB |
esp32s3-n16r8 |
β / 371-433 | β / 8131KB-8292KB | β / 76KB-104KB |
esp32s31 |
β / 394 | β / 16383KB | β / 136KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 6,711 | β / β | β / β |
desktop-macos |
β / 250,000 | β / β | β / β |
desktop-windows |
β / 646-111,111 | β / β | β / β |
esp32 |
β / 3,356-5,000 | β / 72KB-76KB | β / 34KB-54KB |
esp32p4rev1-eth |
β / 428-484 | β / 33120KB-33166KB | β / 280KB-344KB |
esp32p4rev1-eth-wifi |
β / 74.6 | β / 32882KB | β / 248KB |
esp32s3-n16r8 |
β / 457-474 | β / 8131KB-8216KB | β / 76KB-104KB |
esp32s31 |
β / 486 | β / 16383KB | β / 136KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 5,128 | β / β | β / β |
desktop-macos |
β / 250,000 | β / β | β / β |
desktop-windows |
β / 618-111,111 | β / β | β / β |
esp32 |
β / 90.5-2,358 | β / 77KB-119KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 342-398 | β / 33145KB-33174KB | β / 296KB-352KB |
esp32p4rev1-eth-wifi |
β / 54.7 | β / 32896KB | β / 248KB |
esp32s3-n16r8 |
β / 280-475 | β / 8142KB-8299KB | β / 76KB-108KB |
esp32s31 |
β / 387 | β / 16398KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 5,747 | β / β | β / β |
desktop-macos |
β / 250,000 | β / β | β / β |
desktop-windows |
β / 457-111,111 | β / β | β / β |
esp32 |
β / 90.3-2,358 | β / 76KB-119KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 404-444 | β / 33139KB-33166KB | β / 288KB-344KB |
esp32p4rev1-eth-wifi |
β / 26.8 | β / 32885KB | β / 248KB |
esp32s3-n16r8 |
β / 334-474 | β / 8134KB-8292KB | β / 76KB-104KB |
esp32s31 |
β / 427 | β / 16390KB | β / 136KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 6,757 | β / β | β / β |
desktop-macos |
β / 250,000 | β / β | β / β |
desktop-windows |
β / 631-111,111 | β / β | β / β |
esp32 |
β / 90.1-2,353 | β / 74KB-119KB | β / 38KB-92KB |
esp32p4rev1-eth |
β / 4,329-4,608 | β / 33143KB-33166KB | β / 288KB-344KB |
esp32p4rev1-eth-wifi |
β / 1.3 | β / 32831KB | β / 248KB |
esp32s3-n16r8 |
β / 473-2,278 | β / 8144KB-8290KB | β / 76KB-104KB |
esp32s31 |
β / 3,759 | β / 16397KB | β / 136KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
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-docker-arm64 |
β / 3,521 | β / β | β / β |
desktop-macos |
β / 200,000-250,000 | β / β | β / β |
desktop-windows |
β / 676-111,111 | β / β | β / β |
esp32 |
β / 4,310-5,155 | β / 74KB-78KB | β / 34KB-54KB |
esp32p4rev1-eth |
β / 362-389 | β / 33143KB-33174KB | β / 288KB-352KB |
esp32p4rev1-eth-wifi |
β / 66.3 | β / 32897KB | β / 248KB |
esp32s3-n16r8 |
β / 352-473 | β / 8133KB-8279KB | β / 76KB-104KB |
esp32s31 |
β / 334 | β / 16398KB | β / 144KB |
desktop-docker-arm64: observed 2026-09-30desktop-macos: observed 2026-09-24desktop-windows: observed 2026-09-30esp32: observed 2026-09-29esp32p4rev1-eth: observed 2026-09-29esp32p4rev1-eth-wifi: observed 2026-09-29esp32s3-n16r8: observed 2026-09-30esp32s31: observed 2026-09-29
Trailing setup (no measurement after)¶
cleanup-driver(remove_module)cleanup-fx(remove_module)cleanup-layer(remove_module)
SystemModule¶
scenario_System_identity_survives_reboot¶
test/scenarios/core/scenario_System_identity_survives_reboot.json: A device's own identity survives a restart. Names it, restarts, and reads it back: the round trip nothing else proves, because a control that reads correctly while the process still holds it says nothing about what reached the filesystem. This is also the shape every rename risk is checked with, since the sweep is exactly the event that could break a read-back without breaking a write.
Mode: mutate Β· Also touches: FilesystemModule
Trailing setup (no measurement after)¶
name-it(set_control): Set the name a user would set, which is also what mDNS and the access point advertise.reads-back-live(expect_control): It reads back immediately, which only proves the control was written in memory.restart(reboot): Restart, and wait for the device to answer again.survived(expect_control): The name is still there, so it reached the filesystem and was read back at boot.
Uncategorized¶
scenario_Effects_teardown_under_a_running_pipeline¶
test/scenarios/light/scenario_Effects_teardown_under_a_running_pipeline.json: Removing modules while the lights are still rendering. A module that is taken away mid-frame must leave the pipeline running on whatever it can still reach, rather than following a pointer to something that is gone.
Mode: mutate Β· Also touches: Effects, Services, AudioService
Trailing setup (no measurement after)¶
measure-baseline(measure): The pipeline rendering on its own, which every later measurement is read against.add-producer(add_module): An audio producer: it acquires a source and publishes a frame every other audio module reads.add-consumer(add_module): And a consumer of it, under the same layer the rainbow renders into, so two effects composite while a producer feeds one of them.measure-wired(measure): Producer and consumer both live. The difference against the baseline is what the pair costs.remove-the-producer-first(remove_module): THE CASE THIS EXISTS FOR. The microphone goes while the consumer is still rendering: it must fall back to the service's static silence rather than reading through a pointer to a module that has been destroyed.measure-orphaned-consumer(measure): The consumer outlived its producer and the lights are still on. A dangling read shows up here as a crash rather than a number.the-consumer-still-renders(expect_control): Its producer is gone, so it reads the service's static silence. Reporting its own control at all is what says the module is alive rather than dangling.remove-the-orphan(remove_module): And the consumer itself, which is the ordinary half: a module removed with nothing depending on it.measure-back-to-baseline(measure): Both gone. The cost should return to the baseline, since what was added has been fully released rather than merely unhooked.add-an-effect-to-remove(add_module): An effect this scenario owns, since the one the fixture wires exists in-process and never on a device: live, the board is its own fixture and that id was never created here.remove-the-rendering-effect(remove_module): The last effect this scenario added, so the layer is left with a buffer and whatever the board wired itself. An empty layer is a state the pipeline holds rather than a reason to stop.measure-empty-layer(measure): A layer with no effect at all: the floor, and proof the teardown reached it without taking the buffer with it.the-grid-outlived-its-effects(expect_control): Every effect is gone and the layout still reports the size it was built with: the teardown took the effects and left the pipeline standing.add-an-effect-back(add_module): And one back in the same slot, because a pipeline that tears down cleanly but cannot be rebuilt is only half working.measure-rebuilt(measure): Rendering again after a full teardown and rebuild, which is what leaves the buffer checks below something to assert.the-rebuilt-effect-is-there(expect_control): And the new effect answers, which is what makes the rebuild a rebuild rather than an empty slot.add-a-second-driver(add_module): A driver beside the preview, so the container holds one module a user added and one that is apparatus.clear-the-drivers(clear_children): Clear the container rather than naming what to remove, which is how a scenario prepares its own canvas on a device whose tree it did not build. The rule it has to honour is that apparatus stays: a preview, a board or an Improv submodule is not content.the-apparatus-survived-the-clear(expect_control): THE RULE THIS EXISTS FOR. The preview still answers after a clear that took the driver beside it, which is what says clear_children skipped the non-editable submodule rather than emptying the container wholesale.measure-after-the-clear(measure): Still rendering into the preview with the added driver gone.
scenario_Modifiers_reshape_the_mapping¶
test/scenarios/light/scenario_Modifiers_reshape_the_mapping.json: A modifier changes where an effect's pixels land, not what it draws. Stacking, reordering and removing them rebuilds the mapping under a running pipeline, and a layer with no modifier at all must stay on the identity path rather than paying for a lookup table it does not need.
Mode: mutate Β· Also touches: Layer, MultiplyModifier, CheckerboardModifier
Trailing setup (no measurement after)¶
measure-identity(measure): A lone layer with no modifier: one light maps to one light, so there is no lookup table and no separate output buffer. This is the floor the rest is read against, and the contrast that says a modifier costs something.add-a-modifier(add_module): A multiply, which folds the coordinate space: from here the layer needs a lookup table and the drivers need a buffer of their own.the-modifier-is-wired(expect_control): It reports its own control, which is what says the module reached the layer rather than being created beside it.measure-one-modifier(measure): The difference against the identity floor is what the lookup table and the second buffer cost.mirror-it(set_control): A control change on a modifier rebuilds the mapping, where the same change on an effect would not: the pipeline has to fold again on the next frame.the-mirror-took(expect_control)measure-remapped(measure): Rendering through the rebuilt mapping. A rebuild that failed leaves the old table in place, which costs the same and shows the wrong picture.stack-a-second(add_module): A checkerboard on top of the multiply: two modifiers fold in order, so the second reads what the first produced rather than the raw layout.measure-two-deep(measure): A two-deep chain, which is the shape a real installation uses and the one where an ordering fault shows.remove-the-first(remove_module): The multiply goes while the checkerboard stays. The chain has to re-fold from what is left rather than keeping a stale table that names a modifier which no longer exists.the-survivor-is-still-wired(expect_control): The remaining modifier answers, so the removal took one link out of the chain rather than breaking it.measure-refolded(measure): One modifier again, after a removal from the middle of a chain.swap-it(replace_module): And replaced in its own slot, which is a remove and an add the pipeline has to survive as one gesture.the-slot-holds-the-new-type(expect_control): A control only a mirror has, read through the id the checkerboard was registered under. This is what separates a swap from a step that did nothing: the size control the old module answered with is gone, and mirrorY answers in its place.measure-swapped(measure): Rendering through a modifier that was not there a frame ago, in a slot that never emptied.remove-the-last(remove_module): The last modifier goes, so the layer returns to the identity path it started on.back-on-the-identity-path(expect_control): The layout still reports its size with no modifier left, which is what says the fold was released rather than merely bypassed.measure-identity-again(measure): The floor again. Read against the first measurement, this is what says the lookup table and the extra buffer were given back rather than leaked.
scenario_Network_hardware_reconfigures_live¶
test/scenarios/core/scenario_Network_hardware_reconfigures_live.json: The network settings a desktop cannot answer for: an Ethernet PHY cycled on real silicon, mDNS announcing over a real radio, and Home Assistant discovery announced and retracted against a real broker. The render loop has to survive every one of them.
Mode: mutate Β· live-only (skipped in-process) Β· Also touches: NetworkModule, MqttModule
Trailing setup (no measurement after)¶
measure-baseline(measure): The render loop before anything is reconfigured, which every later measurement is read against.mdns-off(set_control): The responder off. On a device this stops real announcement traffic on a real radio, where the desktop advertises nothing at all.mdns-off-took(expect_control)mdns-on(set_control): And back on, which re-registers the service. The cost of that registration is what a desktop cannot show.measure-mdns-cycled(measure): The render loop after a full off-on cycle. A responder that leaks a socket or a task shows here rather than in a unit test.ha-discovery-on(set_control): Home Assistant discovery announced. What is asserted is the device's own side, that the toggle takes and the render loop survives the announce: whether a broker received anything is not something this tier can see, and no check here depends on it.ha-discovery-off(set_control): And retracted, which frees the buffers the announce allocated. The pair is the leak test.ha-discovery-off-took(expect_control)measure-mqtt-cycled(measure): Announce and retract cost, recorded rather than asserted: with a broker the publish is real work, without one the announce is a local no-op, and neither is a number to promise.eth-lan8720(set_control): An Ethernet PHY named on a running device, which calls ethStop and ethInit for real. On the desktop this is a seam that returns immediately.eth-ip101(set_control): A second RMII PHY, which shares the EMAC path but not its pin map.eth-w5500(set_control): The SPI-attached one, a different driver entirely: it tears down the bus rather than the EMAC.eth-none(set_control): Back to none, tearing the interface down again. Cycling is what finds a driver that survives one init but not two.eth-back-to-none(expect_control)measure-eth-cycled(measure): The render loop after the interface came up and went away. A reconfigure that stalls the loop is the fault this exists to catch.restart(reboot): A device that forgets its network settings is one somebody has to set up twice.mdns-survived(expect_control)eth-survived(expect_control)