You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Execution plan for the work scoped in #13 (feasibility, with prototype) and motivated by #10 (license strategy: delete RPSL libhelix_mp3). Implementation PRs land in FastLED/FastLED; this issue is the phase tracker and can be transferred there when work begins. Each phase has an explicit exit gate — no phase starts until the previous gate is green in CI.
Job: vendor minimp3 → optimize to Helix parity → SIMD past Helix → lock in as the only MP3 backend
End state:src/third_party/minimp3/ (CC0, fixed-point capable, SIMD-accelerated) is FastLED's only MP3 decoder; src/third_party/libhelix_mp3/ (RPSL/RCSL) is deleted; the decoder is "locked": golden-tested, fuzzed, memory- and cycle-budgeted in CI so it cannot silently regress.
Ground-truth budgets to beat (Helix, from the #13 audit):
Metric
Helix baseline
Working RAM
~24 KB heap (7 mallocs), tiny stack
Arithmetic
~55–75k multiplies/frame; 64-bit MACs only in polyphase
Dual-backend scaffold:src/fl/codec/mp3.h public API unchanged; backend selected at compile time — FASTLED_MP3_BACKEND_HELIX (default, unchanged) vs FASTLED_MP3_BACKEND_MINIMP3. The five-call Helix wrapper gets a sibling minimp3 wrapper (mp3dec_decode_frame maps 1:1; derive Mp3Info.version from the header bytes since mp3dec_frame_info_t lacks it).
Scratch off the stack immediately (prereq for everything): add mp3dec_decode_frame_r(dec, scratch, …) taking caller-provided mp3dec_scratch_t (16.3 KB); wrapper owns it heap-side. Upstream-friendly patch, kept as a minimal diff file against pristine upstream.
Golden harness v1 (host): one test binary builds Helix + minimp3-float backends; corpus from Feasibility: MINIMP3_FIXED_POINT — CC0 fixed-point minimp3 mode, golden-tested vs float, to replace RPSL libhelix_mp3 #13 (ISO 11172-4/13818-4 vectors, LAME synthetics incl. −60 dBFS sine, joint/intensity stereo, MPEG-2/2.5 low rates, free-format, VBR reservoir stress, resync garbage). Assert identical frame acceptance/sample counts between backends; PCM compared to ISO reference (both must pass limited-accuracy floor) and to each other (report-only PSNR — Helix vs minimp3 are different decoders; the hard golden gate arrives in Phase 3 as fixed-vs-float within minimp3).
Exit gate G0: minimp3-float backend passes the full corpus behaviorally on host CI (Linux + Windows + macOS); public Mp3 API byte-compatible; Helix default untouched; no src/ build regression on any platform (compile-only matrix incl. AVR/WASM).
Phase 1 — Memory audit (native host)
Build the measurement rig before optimizing anything. All instruments run on host; each produces a line in a versioned ledger.
Heap: route all codec allocations through an accounting allocator (fl:: hooks): bytes-current, bytes-peak, alloc-count, per-tag (decoder-state / scratch / stream-buffer). Cross-check with heaptrack or valgrind --tool=massif in the Linux CI job.
Stack: three instruments — (1) -fstack-usage + -Wframe-larger-than= on every codec TU (fails the build over budget); (2) watermark painting in the harness (fill a guard region with a pattern around the decode call, measure high-water); (3) static worst-depth estimate from .su files + call graph. Budget: ≤ 2 KB stack per decode call (everything big must be in owned state/scratch).
Static/flash:nm --size-sort / size on the codec object; table ledger (every const table, bytes, which stage). Fixed-point tables added in Phase 3 must be itemized here.
Ledger:codec_memory_ledger.md checked in, machine-parsed in CI; a PR that regresses any figure > 2% fails unless the ledger is deliberately updated in the same PR (fail-closed, same philosophy as the license tooling).
Record Helix's numbers with the identical rig (it's in-tree — same allocator hooks, same watermarking) so parity claims are apples-to-apples.
Exit gate G1: ledger populated for Helix and minimp3-float; CI regression gate live; scratch-off-stack verified by watermark (< 2 KB stack).
Phase 2 — CPU profiling audit (native host)
Host cycles don't predict MCU cycles — so the rig measures three tiers, from portable to µarch-specific:
Tier 1 — arithmetic-op ledger (the MCU-honest metric): instrument the multiply primitives (MULSHIFT32, 64-bit MAC, float mul in the float build) with audit-build counters. Decode the corpus; report exact multiplies/frame and MACs/frame per stage, for Helix and minimp3 alike. This is the metric parity is defined on (Feasibility: MINIMP3_FIXED_POINT — CC0 fixed-point minimp3 mode, golden-tested vs float, to replace RPSL libhelix_mp3 #13 projection: 55–75k/frame) and it is host-independent.
Tier 3 — codegen inspection for MCU ISAs: cross-compile the codec TU with -Os for xtensa-esp32-elf, riscv32-esp-elf, arm-none-eabi -mcpu=cortex-m0plus and -mcpu=cortex-m4; objdump the inner loops (polyphase, DCT32); record inner-loop instruction counts in the ledger. Catches "host looked fine, M0+ emits a 6-instruction libcall per MAC" early — no hardware needed.
Baseline all three tiers for Helix with the same rig.
Exit gate G2: all three tiers automated in CI; Helix baseline recorded; minimp3-float baseline recorded (expect it to lose badly — that's the point of the ledger).
Phase 3 — Fixed-point conversion to parity (the #13 staged plan, run under the audit rigs)
Stage order and design per #13 (synthesis back-end → IMDCT/antialias → Huffman/dequant/scalefactors → stereo + guard-bit tracking end-to-end; MINIMP3_ONLY_MP3 for v1). Clean-room rules from #13 §Clean-room apply (Helix = ideas only; tables regenerated from ISO formulas; generator scripts committed).
Golden gate hardens: fixed-vs-float within minimp3 — PSNR ≥ 90 dB and ≤ 8 LSB on every corpus file; −60 dB signal-relative gate ≥ 55 dB; per-stage MINIMP3_STAGE_DUMP epsilons; differential fuzz (both modes agree accept/reject + PCM, ASan/UBSan clean).
Each converted stage must land with: golden green, memory ledger delta itemized, op-ledger delta itemized.
Scalar optimization pass to close any gap to Helix parity: early-terminating polyphase multiplies (Helix's pre-shifted-coefficient concept), table layout for cache/flash-line locality, sync-scan cost reduction, buffer reuse between granules.
Exit gate G3 ("Helix parity"): on the op-ledger, multiplies/frame ≤ 1.1× Helix; working RAM ≤ 24 KB; stack ≤ 2 KB; all golden + fuzz gates green; Tier-3 inner-loop instruction counts within 1.2× of Helix's on all four ISAs. One on-target spot-check (ESP32 + RP2040 cycles/frame) to validate that the host proxies track reality — the only non-host step in the audit plan, run once at this gate.
Phase 4 — SIMD: beat Helix
Helix has zero SIMD — this is the phase that makes replacement strictly better, not just license-clean. Integer SIMD is bit-exact vs scalar fixed-point, so the golden gate here is exact equality, the strongest possible lock.
Host lanes first (prove the pattern where profiling is easy): SSE2 and NEON int32 paths for the dominant costs — polyphase (~37k MACs/frame), DCT32, IMDCT butterflies — following minimp3's existing float-SIMD scaffolding style (MINIMP3_NO_SIMD opt-out preserved).
Embedded flagship — ESP32-S3 PIE (128-bit vector unit): polyphase + DCT32 kernels via esp-dsp-style intrinsics/asm, guarded by chip detect at compile time.
Optional stretch: Cortex-M4/M7 DSP extension scheduling (SMULL/SMLAL pipelines; evaluate whether dual-16 SMLAD fits anywhere without precision loss — likely not for Q25 data; document the finding either way).
Every SIMD kernel: bit-exact vs scalar fixed on the full corpus (exact-equality golden), covered by the differential fuzzer, and toggleable at compile time.
Exit gate G4 ("faster than Helix"): host cycles/frame ≥ 1.5× faster than Helix-scalar on x86-64 and Apple/ARM CI runners; ESP32-S3 ≥ 1.3× faster than Helix on-target; no target anywhere slower than the scalar fixed path; bit-exactness green.
Phase 5 — Swap & lock-in
Flip the default: FASTLED_MP3_BACKEND_MINIMP3 becomes the default; Helix demoted to FASTLED_MP3_HELIX_LEGACY opt-in with a deprecation warning, for one minor release.
Delete src/third_party/libhelix_mp3/ + the .S srcFilter line in library.json; update third-party manifest, per-file markers, REUSE/SBOM output; note the removal in release notes ("FastLED no longer ships any RPSL/RCSL code").
Lock the decoder in place: golden corpus + fuzz smoke + memory ledger + cpu trend + bit-exactness gates all become required PR checks for any file under src/third_party/minimp3/ or src/fl/codec/mp3*; CODEOWNERS on those paths; the vendored diff-against-upstream kept minimal and documented.
Upstream PR to lieff/minimp3 (MINIMP3_FIXED_POINT + _r scratch API + SIMD lanes), CC0. If upstream is dormant, FastLED carries it in-tree as CC0 with the diff file as the contract.
Exit gate G5 (done): Helix tree deleted on main; all lock gates required in CI; one release shipped with minimp3 default and no regression reports on the audio-reactive examples.
Sequencing, effort, risks
Dependencies: G0→G1→G2 can overlap (rigs are independent); G3 depends on G1+G2 (can't claim parity without both ledgers); G4 depends on G3 (SIMD over the fixed kernels); G5 depends on G4.
Effort:#13 estimated ~6 pw for the conversion; the audit rigs add ~1.5 pw; SIMD ~2 pw (host lanes + S3); swap/lock ~0.5 pw → ~10 person-weeks + 2 contingency.
Top risks (carried from #13, plus new): (1) dynamic-range corners in the guard-bit budget → −60 dB gates + stage hooks; (2) M0+ 64-bit-MAC budget → Tier-3 codegen ledger catches it at G2/G3, measured 32-bit-MAC fallback ready; (3) S3 PIE toolchain friction (intrinsics maturity) → treat S3 SIMD as the G4 flagship but allow G4-host to gate independently if PIE slips one milestone; (4) upstream divergence — pristine-upstream + minimal-diff discipline keeps rebases cheap.
Note
Execution plan for the work scoped in #13 (feasibility, with prototype) and motivated by #10 (license strategy: delete RPSL
libhelix_mp3). Implementation PRs land inFastLED/FastLED; this issue is the phase tracker and can be transferred there when work begins. Each phase has an explicit exit gate — no phase starts until the previous gate is green in CI.Job: vendor minimp3 → optimize to Helix parity → SIMD past Helix → lock in as the only MP3 backend
End state:
src/third_party/minimp3/(CC0, fixed-point capable, SIMD-accelerated) is FastLED's only MP3 decoder;src/third_party/libhelix_mp3/(RPSL/RCSL) is deleted; the decoder is "locked": golden-tested, fuzzed, memory- and cycle-budgeted in CI so it cannot silently regress.Ground-truth budgets to beat (Helix, from the #13 audit):
Phase 0 — Vendor & scaffold (no behavior change)
minimp3.h(+minimp3_ex.hif needed for tests only) intosrc/third_party/minimp3/following FastLED conventions:namespace fl { namespace third_party { … } }wrap,FL_NO_EXCEPT,MINIMP3_NO_STDIO, unity-build integration (_build.cpp.hpp→src/fl/build/third_party+.cpp).LICENSE(CC0-1.0) in the component dir + third-party manifest entry + per-file markers per the universal-marker policy (Strategy: GPL/free-software compatibility (§11.8 + §11.9 Public Combination Exception) and universal per-file license markers #10). Record upstream commit SHA inPROVENANCE-style note.src/fl/codec/mp3.hpublic API unchanged; backend selected at compile time —FASTLED_MP3_BACKEND_HELIX(default, unchanged) vsFASTLED_MP3_BACKEND_MINIMP3. The five-call Helix wrapper gets a sibling minimp3 wrapper (mp3dec_decode_framemaps 1:1; deriveMp3Info.versionfrom the header bytes sincemp3dec_frame_info_tlacks it).mp3dec_decode_frame_r(dec, scratch, …)taking caller-providedmp3dec_scratch_t(16.3 KB); wrapper owns it heap-side. Upstream-friendly patch, kept as a minimal diff file against pristine upstream.Exit gate G0: minimp3-float backend passes the full corpus behaviorally on host CI (Linux + Windows + macOS); public
Mp3API byte-compatible; Helix default untouched; nosrc/build regression on any platform (compile-only matrix incl. AVR/WASM).Phase 1 — Memory audit (native host)
Build the measurement rig before optimizing anything. All instruments run on host; each produces a line in a versioned ledger.
fl::hooks): bytes-current, bytes-peak, alloc-count, per-tag (decoder-state / scratch / stream-buffer). Cross-check withheaptrackorvalgrind --tool=massifin the Linux CI job.-fstack-usage+-Wframe-larger-than=on every codec TU (fails the build over budget); (2) watermark painting in the harness (fill a guard region with a pattern around the decode call, measure high-water); (3) static worst-depth estimate from.sufiles + call graph. Budget: ≤ 2 KB stack per decode call (everything big must be in owned state/scratch).nm --size-sort/sizeon the codec object; table ledger (every const table, bytes, which stage). Fixed-point tables added in Phase 3 must be itemized here.codec_memory_ledger.mdchecked in, machine-parsed in CI; a PR that regresses any figure > 2% fails unless the ledger is deliberately updated in the same PR (fail-closed, same philosophy as the license tooling).Targets: working RAM (persistent + scratch + stream buffer) ≤ 24 KB (parity), stretch ≤ 20 KB; flash tables ≤ Helix's table footprint + 20%.
Exit gate G1: ledger populated for Helix and minimp3-float; CI regression gate live; scratch-off-stack verified by watermark (< 2 KB stack).
Phase 2 — CPU profiling audit (native host)
Host cycles don't predict MCU cycles — so the rig measures three tiers, from portable to µarch-specific:
MULSHIFT32, 64-bit MAC, float mul in the float build) with audit-build counters. Decode the corpus; report exact multiplies/frame and MACs/frame per stage, for Helix and minimp3 alike. This is the metric parity is defined on (Feasibility: MINIMP3_FIXED_POINT — CC0 fixed-point minimp3 mode, golden-tested vs float, to replace RPSL libhelix_mp3 #13 projection: 55–75k/frame) and it is host-independent.perf stat(cycles, instructions, IPC, branch misses) +valgrind --tool=callgrindper-function attribution; per-stage wall-time via theMINIMP3_STAGE_DUMPhooks' timer variant. Pinned governor, deterministic corpus, N=30 runs, report medians. Trend file (codec_cpu_trend.json) tracked in CI; ±5% regression gate.-Osforxtensa-esp32-elf,riscv32-esp-elf,arm-none-eabi -mcpu=cortex-m0plusand-mcpu=cortex-m4;objdumpthe inner loops (polyphase, DCT32); record inner-loop instruction counts in the ledger. Catches "host looked fine, M0+ emits a 6-instruction libcall per MAC" early — no hardware needed.Exit gate G2: all three tiers automated in CI; Helix baseline recorded; minimp3-float baseline recorded (expect it to lose badly — that's the point of the ledger).
Phase 3 — Fixed-point conversion to parity (the #13 staged plan, run under the audit rigs)
Stage order and design per #13 (synthesis back-end → IMDCT/antialias → Huffman/dequant/scalefactors → stereo + guard-bit tracking end-to-end;
MINIMP3_ONLY_MP3for v1). Clean-room rules from #13 §Clean-room apply (Helix = ideas only; tables regenerated from ISO formulas; generator scripts committed).MINIMP3_STAGE_DUMPepsilons; differential fuzz (both modes agree accept/reject + PCM, ASan/UBSan clean).Exit gate G3 ("Helix parity"): on the op-ledger, multiplies/frame ≤ 1.1× Helix; working RAM ≤ 24 KB; stack ≤ 2 KB; all golden + fuzz gates green; Tier-3 inner-loop instruction counts within 1.2× of Helix's on all four ISAs. One on-target spot-check (ESP32 + RP2040 cycles/frame) to validate that the host proxies track reality — the only non-host step in the audit plan, run once at this gate.
Phase 4 — SIMD: beat Helix
Helix has zero SIMD — this is the phase that makes replacement strictly better, not just license-clean. Integer SIMD is bit-exact vs scalar fixed-point, so the golden gate here is exact equality, the strongest possible lock.
MINIMP3_NO_SIMDopt-out preserved).SMULL/SMLALpipelines; evaluate whether dual-16SMLADfits anywhere without precision loss — likely not for Q25 data; document the finding either way).Exit gate G4 ("faster than Helix"): host cycles/frame ≥ 1.5× faster than Helix-scalar on x86-64 and Apple/ARM CI runners; ESP32-S3 ≥ 1.3× faster than Helix on-target; no target anywhere slower than the scalar fixed path; bit-exactness green.
Phase 5 — Swap & lock-in
FASTLED_MP3_BACKEND_MINIMP3becomes the default; Helix demoted toFASTLED_MP3_HELIX_LEGACYopt-in with a deprecation warning, for one minor release.src/third_party/libhelix_mp3/+ the.SsrcFilter line inlibrary.json; update third-party manifest, per-file markers, REUSE/SBOM output; note the removal in release notes ("FastLED no longer ships any RPSL/RCSL code").src/third_party/minimp3/orsrc/fl/codec/mp3*; CODEOWNERS on those paths; the vendored diff-against-upstream kept minimal and documented.MINIMP3_FIXED_POINT+_rscratch API + SIMD lanes), CC0. If upstream is dormant, FastLED carries it in-tree as CC0 with the diff file as the contract.LEGAL-REVIEW.md's standing-exception note for libhelix.Exit gate G5 (done): Helix tree deleted on main; all lock gates required in CI; one release shipped with minimp3 default and no regression reports on the audio-reactive examples.
Sequencing, effort, risks
Dependencies: G0→G1→G2 can overlap (rigs are independent); G3 depends on G1+G2 (can't claim parity without both ledgers); G4 depends on G3 (SIMD over the fixed kernels); G5 depends on G4.
Effort: #13 estimated ~6 pw for the conversion; the audit rigs add ~1.5 pw; SIMD ~2 pw (host lanes + S3); swap/lock ~0.5 pw → ~10 person-weeks + 2 contingency.
Top risks (carried from #13, plus new): (1) dynamic-range corners in the guard-bit budget → −60 dB gates + stage hooks; (2) M0+ 64-bit-MAC budget → Tier-3 codegen ledger catches it at G2/G3, measured 32-bit-MAC fallback ready; (3) S3 PIE toolchain friction (intrinsics maturity) → treat S3 SIMD as the G4 flagship but allow G4-host to gate independently if PIE slips one milestone; (4) upstream divergence — pristine-upstream + minimal-diff discipline keeps rebases cheap.