From ed4a08bb6249ef1480f1040bad52e49b1224f14b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 00:50:30 +0000 Subject: [PATCH] Sample-transfer start: flush due deltas only, not the sequencer tick amy_parse_transfer_layer_message() called the full amy_execute_deltas() before pcm_load() (4a480927), so that a queued reset lands first -- amy_reset_oscs() unloads all PCM presets, and would otherwise wipe the new sample when it played. But that call runs on the sending thread, and amy_execute_deltas() also advances the sequencer (an unlocked read-modify-write of next_amy_tick_us that races the render thread's own tick, and can run the external sequencer hook off the audio thread) and polls CV inputs. 2b2876b9 fixed the same thing for patch loads. Add amy_settle_deltas(): the render lock plus flush_due_deltas(), nothing else, safe from any thread. Use it for the sample-transfer start, the patch-load path (which open-coded the same three lines), and amy_execute_deltas() itself. Fixes #1207. make ctest passes; make test gives the same 90 pass / 43 fail as before here, with TestLoadSample (reset, then load a sample) matching its reference; ASan patch-reload stress 5/5 clean. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UW4jqUQ4Xc1sdoqzUPL2YV --- src/amy.c | 26 ++++++++++++++++---------- src/amy.h | 1 + src/parse.c | 6 +++++- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/amy.c b/src/amy.c index e3897ba8..6d93239b 100644 --- a/src/amy.c +++ b/src/amy.c @@ -890,9 +890,7 @@ void amy_event_to_deltas_queue(amy_event *e, uint16_t base_osc, uint16_t oscs_pe // The load itself does not: it takes tens of ms on an ESP32-S3 // (a 6-voice DX7 load ~50-70 ms), and a render held off that long // runs the DMA ring dry. The flush is µs. - amy_grab_render_lock(); - flush_due_deltas(); - amy_release_render_lock(); + amy_settle_deltas(); patches_load_patch(e); } // Execute any other commands in this event. @@ -2445,6 +2443,17 @@ static void flush_due_deltas() { amy_release_lock(); } +// Play the deltas that are due, from any thread, without advancing the +// sequencer: what ingest calls before an operation that a queued reset must +// not land after (a patch load rebuilding the synth tables, a sample load +// that amy_reset_oscs() would unload). Under the render lock, because the +// flush can free oscs a render in progress is reading. +void amy_settle_deltas() { + amy_grab_render_lock(); + flush_due_deltas(); + amy_release_render_lock(); +} + // this takes scheduled deltas and plays them at the right time void amy_execute_deltas() { AMY_PROFILE_START(AMY_EXECUTE_DELTAS) @@ -2453,13 +2462,10 @@ void amy_execute_deltas() { sequencer_check_and_fill(); // Make sure any CV-triggered events are added to delta queue update_external_cv_in(); - // The flush can free oscs, so it runs under the render lock. Render loops - // already hold it across the whole block (it's recursive for them); this - // covers callers that execute deltas off the render thread (parse.c's - // sample-transfer start). - amy_grab_render_lock(); - flush_due_deltas(); - amy_release_render_lock(); + // Render loops already hold the render lock across the whole block (it's + // recursive for them); amy_settle_deltas() takes it anyway, for render + // loops that call this before taking it for the render itself. + amy_settle_deltas(); AMY_PROFILE_STOP(AMY_EXECUTE_DELTAS) } diff --git a/src/amy.h b/src/amy.h index 2f65e7fe..77277fff 100644 --- a/src/amy.h +++ b/src/amy.h @@ -1206,6 +1206,7 @@ void patches_deinit(); void parse_algo_source(char* message, int16_t *vals); void hold_and_modify(uint16_t osc) ; void amy_execute_deltas(); +void amy_settle_deltas(); // due deltas only, any thread: no sequencer tick int16_t * amy_fill_buffer(); int16_t * amy_simple_fill_buffer(); // excute_deltas + render + fill_buffer uint32_t ms_to_samples(uint32_t ms) ; diff --git a/src/parse.c b/src/parse.c index f29926c6..0eb87230 100644 --- a/src/parse.c +++ b/src/parse.c @@ -588,7 +588,11 @@ uint16_t amy_parse_transfer_layer_message(char *message) { if(sm[1]==0) { // remove preset pcm_unload_preset(sm[0]); } else { - amy_execute_deltas(); + // A queued reset must land before the load, or amy_reset_oscs() + // unloads the new sample when it plays (pcm_unload_all_presets). + // Only the flush: this runs on the sending thread, and advancing + // the sequencer here would race the render thread's own tick. + amy_settle_deltas(); int16_t * ram = pcm_load(sm[0], sm[1], sm[2], 1, midinote, sm[4], sm[5]); start_receiving_transfer(sm[1]*2, (uint8_t*)ram); }