Phase 2 (L34, L35). Both findings' line citations were accurate this time.
- L34 data race: process() (Rack's audio thread) called
imlShadow.get_example_features()/get_example_labels() directly on the WORKER
thread's private engine — which the file's own THREADING INVARIANT comment
says only the worker may touch — while workerLoop() concurrently
clear/refills those same std::vector<std::vector<float>> members via
load_examples(), train_(), randomise_weights and clear_dataset. Unsynchronised
reader/writer on a non-atomic vector: undefined behaviour.
Fix extends the staged handoff the file ALREADY uses for pendingWeights
rather than adding a second mutex: the worker deep-copies features/labels
into pendingFeatures/pendingLabels at the same instant it copies
pendingWeights, immediately before weightsPending.store(true), and the flag is
now released only after the whole batch is consumed — closing an early-release
window the old code had. process() no longer references imlShadow at all
(verified: the only surviving mention is a comment).
- L35: process() ran full jansson serialize on every weight-swap OSC push and
full json_loads + dataFromJson on incoming OSC state — heap-heavy tree work
on the audio thread. The plan said to move it to the worker. It is DELETED
instead: reading the actual transport shows both directions talk to nobody —
osc-client.ts only ever sends {params|input|feedback}, and bridge.ts has no
state/weights case and explicitly drops other addresses. Relocating
heap-heavy work to serve a confirmed-zero consumer is complexity without a
requirement; removing the cause is the smaller coherent design.
dataToJson/dataFromJson are UNTOUCHED — they remain the live consumers for
Rack patch save/load and the .nisps preset menu, both off the audio thread.
Neither is empirically reproduced: a real race needs a live Rack engine under
TSan, which is not available here. Justified by reading, and verified by
`cd vcv && make -j4` (clean) plus the host suite including
test_vcv_iml_parity.cpp, which pins iml.hpp bit-exactly against the core MLP —
iml.hpp was not modified, and parity holds.
Known remaining, pre-existing and out of scope: process() still takes a brief
lock_guard on feedbackMutex to copy a small staged struct, and several config
fields (slewMs, oscPort, output/input range flags) are written by the UI thread
without atomics.