Skip to content

Execution plan: vendor minimp3, optimize to Helix parity, SIMD past Helix, delete libhelix_mp3 (phased, gated) #14

Description

@zackees

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 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
Realtime record ESP32 LX6/LX7, ESP32-C3, RP2040, Cortex-M3/M4 — proven
SIMD none anywhere — this is how we beat it

Phase 0 — Vendor & scaffold (no behavior change)

  • Vendor minimp3.h (+ minimp3_ex.h if needed for tests only) into src/third_party/minimp3/ following FastLED conventions: namespace fl { namespace third_party { … } } wrap, FL_NO_EXCEPT, MINIMP3_NO_STDIO, unity-build integration (_build.cpp.hppsrc/fl/build/third_party+.cpp).
  • Ship 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 in PROVENANCE-style note.
  • 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.

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:

  • 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 2 — host attribution: perf stat (cycles, instructions, IPC, branch misses) + valgrind --tool=callgrind per-function attribution; per-stage wall-time via the MINIMP3_STAGE_DUMP hooks' timer variant. Pinned governor, deterministic corpus, N=30 runs, report medians. Trend file (codec_cpu_trend.json) tracked in CI; ±5% regression gate.
  • 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.
  • Run the Feasibility: MINIMP3_FIXED_POINT — CC0 fixed-point minimp3 mode, golden-tested vs float, to replace RPSL libhelix_mp3 #13 acceptance gate in full: golden suite green (ISO floor + internal PSNR gates), 72 h differential fuzz zero-divergence, on-target budgets (≤ 60% core on ESP32-C3@160 MHz and RP2040@133 MHz, ≤ 25% on ESP32@240 MHz for 44.1 kHz stereo 320 kbps), A/B listening sanity on demo content.
  • 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.
  • Close the loop on the license side: update Strategy: GPL/free-software compatibility (§11.8 + §11.9 Public Combination Exception) and universal per-file license markers #10 (libhelix resolved — RPSL tree deleted), third-party audit row in the universal-marker migration, and 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions