One cut, no dual path. Closes ALIGNMENT defect 3 ("Arduino-CLI build
machinery is actively hostile") and vision bullet 4.
platformio.ini carries 16 [env:], one per variant, each passing
-DMEMLNAUT_MODE_TYPE; selftest passes -DNISPS_SELFTEST=1 instead. The env list
IS the registry now — the .ino comment-registry and the NISPS_ST_* token-paste
table are deleted rather than migrated. L12 noted that table was already
silently missing the currently-shipped SLPWorkshop variant, which is the whole
argument against having a second list.
Also deleted: the Python/sed machinery that rewrote the COMMITTED .ino on every
build, the sketch symlink forest, the global TFT_eSPI User_Setup.h mutation
(now -D flags — TFT_eSPI's own documented PlatformIO recipe), the UF2
boot-mount detection stack (upload_protocol=picotool talks to the bootloader
directly), and build-firmware-arch.sh entirely. Scripts 683 -> 435 lines.
memllib is vendored at lib/memllib/ from upstream e291192; no submodules
remain. VENDORED.md records provenance and the re-sync procedure.
S9: a firmware-build CI job compiles three representative envs against a cached
toolchain and reports per-variant flash/RAM. Firmware is in an automated gate
for the FIRST time. The old ci.yml comment justified excluding it as "low
verification value" — an assessment that did not survive contact, since the
SelfTest variant sat broken for an unknown period calling a DisplayDriver
method that did not exist at the pinned memllib commit, and nothing noticed
because nothing built it.
Verified: all 16 envs build from an empty cache, each within ~520 bytes of the
arduino-cli binary it replaces, flash and RAM. Measured as .text+.rodata /
.data+.bss+vector+uninitialized — NOT PlatformIO's console line, which
double-counts .data on this board. This does not prove the hardware boots; no
flash+smoke test was possible and that stays an operator chokepoint.
slpworkshop 248232/145028 pafsynth 256880/149716 selftest 216228/17960
(all 16 in the CI log format; none exceeds 2% of a 16 MB flash)
Two traps recorded so nobody rediscovers them: vendoring memllib's subdirs
without a src/ wrapper makes PlatformIO's library builder silently compile
NOTHING while still linking; and project build_flags land BEFORE the
framework's own -std=gnu++17 -Os, so build_unflags is required.
CORRECTION carried in this commit: the firmware sizes in c19d846's message and
the first version of the memllib recon doc were wrong — SLPWorkshop 145348,
PAFSynth 145300, SelfTest 141840. They came from building variants in sequence
through a SHARED incremental arduino-cli build directory, which reused stale
objects and under-reported by ~75 KB. Clean-cache rebuilds of the identical
commit give 216736/18492 for SelfTest. The real cost of the memllib upstream
bump is +216 bytes flash, not +316. Never measure firmware size through a
reused build dir.
HISTORY NOTE: this commit and the docs commit before it were rebuilt (force-push,
2026-07-21) so that each contains only what its message describes. The first
versions had the firmware deletions stranded in the docs commit by a shared-index
race between concurrent agents; content is byte-identical to the originals.
Gates: run-all-tests.sh ALL GREEN (nisps/ untouched by this change beyond
include paths); 16/16 pio envs build.
126 lines
7 KiB
Markdown
126 lines
7 KiB
Markdown
---
|
|
kind: finding
|
|
date: 2026-07-21
|
|
---
|
|
|
|
# memllib usage inventory — the prerequisite for the §7.5 ownership decision
|
|
|
|
_Prerequisite named by `../plans/simplification-plan.md` §5: "inventory which memllib surface
|
|
(AudioDriver, MEMLNaut board, display/menu, MIDI) is load-bearing" before choosing between a
|
|
fork-pin and vendoring. The operator's §7.5 decision is **vendor the parts we use** ("everything
|
|
should be self contained in this repo"). This document is the evidence that decision needs, plus one
|
|
consequence that was not visible when it was made._
|
|
|
|
## Method
|
|
|
|
Not grep. The firmware was built for real (`arduino-cli`, `rp2040:rp2040:solderparty_rp2350_stamp_xl`,
|
|
SLPWorkshop variant) and the **linker map** was read: `MEMLNaut-NISPS.ino.map` distinguishes
|
|
translation units that merely compiled from those contributing sections to the final image. This is
|
|
the same method that would have caught the daisysp finding (S8) mechanically instead of by reading.
|
|
|
|
## Result: the compile surface is 24 TUs, and all 24 are linked
|
|
|
|
| subdir | .cpp linked / present | headers | size |
|
|
|---|---|---|---|
|
|
| `audio/` | 4 / 4 | 12 | 132K |
|
|
| `hardware/` | 7 / 7 | 21 | 176K |
|
|
| `interface/` | 8 / 8 | 8 | 108K |
|
|
| `synth/` | 3 / 3 | 10 | 1.3M |
|
|
| `utils/` | 2 / 2 | 9 | 80K |
|
|
| **total** | **24 / 24** | **60** | **~1.8M** |
|
|
|
|
Every translation unit reachable through the sketch-tree symlinks contributes kept sections to the
|
|
image — **no discarded-only TU exists**. memllib is therefore *not* a daisysp situation (41 .cpp
|
|
compiled, zero consumers). It is genuinely load-bearing.
|
|
|
|
The library has 30 `.cpp` in total; the 6 not counted above live outside the five symlinked subdirs
|
|
(e.g. `examples/`) and are never compiled by the firmware build. Those are the only part safe to
|
|
drop outright.
|
|
|
|
Firmware's direct include surface is just six entry points — `audio/AudioDriver.hpp`,
|
|
`hardware/memlnaut/MEMLNaut.hpp`, `hardware/memlnaut/Pins.hpp`,
|
|
`hardware/memlnaut/display/{View,SingleSelectView}.hpp`, `interface/MIDIInOut.hpp`, `PicoDefs.hpp`,
|
|
`utils/perf.hpp` — but the transitive closure is the whole of those five subdirs.
|
|
|
|
**Consequence for vendoring: "the load-bearing subset" is ~1.8 MB / 84 files, i.e. essentially all
|
|
of memllib minus `examples/`.** There is no small subset to lift. `synth/` alone is 1.3 MB (mostly
|
|
`maximilian.cpp`) and all three of its TUs link.
|
|
|
|
## The fork divergence — and the correction to this document's first version
|
|
|
|
`src/memllib` is not a third-party dependency. It is **the lab's shared library**
|
|
(`MusicallyEmbodiedML/memllib`), and our fork was 3 commits ahead / 31 behind.
|
|
|
|
**The first version of this document (commit `7a30da9`) drew the wrong conclusion from that, and
|
|
recommended a rebase-then-vendor on the strength of it. Two facts checked afterwards overturn it:**
|
|
|
|
1. **All three of our commits touch only `examples/`** — `bf9691c` (swap memlp includes for
|
|
nisps/core, inline RL utilities), `32cc831` (newlib RNG fix), `b37fc53` (seed-helper rename).
|
|
`examples/` is **not in the sketch symlink forest** (`firmware/MEMLNaut-NISPS/src/` symlinks
|
|
exactly `audio hardware interface synth utils PicoDefs.hpp`), so it is never compiled. Those
|
|
commits existed to let the RL code build against `nisps/core` **while it was being ported**, and
|
|
that port is finished: `nisps/ml/{jolt,ou_noise,feedback,geo_push}.hpp` cite the upstream sources
|
|
directly. So there is nothing of ours to carry forward, and **no rebase to perform** — vendoring
|
|
the five linked subdirs drops `examples/` and our three commits with it.
|
|
2. **Two of the "31 commits of work we are missing" were already absorbed.** `nisps/ml/jolt.hpp:3`
|
|
cites `9fcd459 "jolts"` and `nisps/ml/ou_noise.hpp:3` cites `d0d8a72 "noise"` as their sources.
|
|
The alarm in the first version — "the 31 commits contain work that sounds directly relevant" —
|
|
was therefore overstated for exactly the two commits it named.
|
|
|
|
So the real question was never "rebase or not" but **"which snapshot do we vendor: the pinned
|
|
`188496d` we build today, or current upstream?"**
|
|
|
|
## The staleness was already costing us
|
|
|
|
Upstream `main` **has `DisplayDriver::NavigateToView`** (`display/DisplayDriver.hpp:54`); the pinned
|
|
commit does not. The SelfTest firmware variant called it and had been failing to compile — fixed in
|
|
`b953681` by routing around the missing method. That variant was not written against broken code; it
|
|
was written against a **newer memllib than the pin**. The gap is not theoretical.
|
|
|
|
Upstream also carries `e291192 "l r input swap"`, a **hardware bug fix**: the physical L/R input
|
|
sockets are wired to the opposite codec ADC channels, so `AudioDriver.cpp` now swaps them at the
|
|
lowest level. Every mode on the pinned commit sees its stereo input backwards.
|
|
|
|
## Verified: current upstream builds, and costs almost nothing
|
|
|
|
Submodule moved to `e291192` (upstream `main`).
|
|
|
|
**CORRECTION (2026-07-21, same day).** The first version of this table, and the numbers in commit
|
|
`c19d846`'s message, were WRONG — SLPWorkshop 145348, PAFSynth 145300, SelfTest 141840, with a
|
|
"+316 uniform delta". They were produced by building three variants in a row through a **shared
|
|
incremental `arduino-cli` build directory** (`/tmp/memlnaut-firmware-build`), which reused stale
|
|
objects and under-reported by roughly 75 KB. The error surfaced when the PlatformIO migration could
|
|
not reproduce them; a clean-cache rebuild of the identical commit in a throwaway worktree confirmed
|
|
it. **Never measure firmware size through a reused build directory.**
|
|
|
|
Measured properly (clean build dir per measurement, SelfTest variant):
|
|
|
|
| memllib pin | flash | RAM |
|
|
|---|---|---|
|
|
| `b37fc53` (old pin, pre-bump) | 216520 | 18480 |
|
|
| `e291192` (upstream main) | 216736 | 18492 |
|
|
| **delta** | **+216** | **+12** |
|
|
|
|
The conclusion is unchanged and if anything stronger: the 31-commit upstream bump costs ~216 bytes
|
|
of a 16 MB flash. The bulky new upstream code (`GrainDelayI16`, `ReverbI16`, `ModFXI16`,
|
|
`CCSelectView`, `NameInputView`, `RLView`, `VUMeterView`, `PSRAMManager`) is header-only and
|
|
unreferenced, so the linker drops all of it; the delta is the AudioDriver/DisplayDriver changes.
|
|
|
|
Absolute sizes for every variant, post-bump, are in the Phase 4 migration commit — all 16 build,
|
|
and none exceeds 2% of flash.
|
|
|
|
Exactly **one** compile error had to be fixed: `MEMLNaut-NISPS.ino:169` used `kSampleRate` in a
|
|
`constexpr`, and upstream `1997699 "mode sample rate"` made it a runtime `extern size_t` so a mode
|
|
can choose its own rate. `constexpr` → `const`; it is a once-per-second diagnostic print.
|
|
|
|
## Where this leaves the vendoring
|
|
|
|
Vendor **from current upstream**, not from the old pin. Take the five linked subdirs +
|
|
`PicoDefs.hpp`; drop `examples/` (~1.8 MB → the vendored surface). Record the exact upstream commit
|
|
so a future re-sync is a documented diff rather than an archaeology exercise.
|
|
|
|
The submodule now points at **upstream** rather than the fork: with the pin moved to an upstream
|
|
commit, the fork has nothing the firmware compiles. The fork's `feat/nisps-core-swap` branch remains
|
|
pushed, so nothing is destroyed.
|
|
|
|
This unblocks the PlatformIO migration (plan §5).
|