Skip to content

Take the queue lock when ingest allocates an osc - #1185

Open
bwhitman wants to merge 1 commit into
mainfrom
ingest-alloc-lock
Open

bwhitman wants to merge 1 commit into
mainfrom
ingest-alloc-lock

Conversation

@bwhitman

Copy link
Copy Markdown
Collaborator

`amy_event_to_deltas_queue()` allocates the osc an event addresses (and a chained/mod osc it references) on whatever thread sent the event, without the queue lock. `FREE_OSC` is executed on the render thread inside `flush_due_deltas()`, which does hold it.

A patch load queues frees for the old voice's oscs, then allocates the same osc numbers for the new voice. So the render thread can free an osc while the sending thread is still inside `alloc_osc()` / `reset_osc()`. The result is either a store through NULL at `synth[i]->osc = i` (amy.c reset_osc), or writes into freed memory.

Seen on: a dual-core ESP32-P4, where a synth's patch is reloaded from the application core while the render task runs on the other core. The failure was a Store access fault in reset_osc (stack: amy_parse_message → patches_load_patch → parse_patch_string_to_queue → amy_event_to_deltas_queue → ensure_osc_allocd → alloc_osc → reset_osc), roughly once in every six reloads.

The change: the two ingest-path allocations now go through ensure_osc_allocd_ingest(), which takes the lock around ensure_osc_allocd(). The ingest path never holds the lock on entry (it takes it again in add_delta_to_queue()), so this can't self-deadlock. Render-thread callers are unchanged.

Tested: after the change, 30 reloads in a row on the same board ran clean. Before it, the fault appeared by the 6th.

🤖 Generated with Claude Code

amy_event_to_deltas_queue() allocates the addressed osc (and a referenced
chained/mod osc) on whatever thread sent the event, without the queue
lock. FREE_OSC runs on the render thread inside flush_due_deltas(), which
holds it. A patch load queues frees for the old voice's oscs and then
allocates the same osc numbers for the new voice, so the render thread
can free an osc while the sending thread is still in alloc_osc() /
reset_osc() -- a store through NULL at reset_osc's synth[i]->osc = i, or
writes into freed memory.

Seen on a dual-core ESP32-P4 reloading a synth's patch from the
application core while the render task runs on the other: a Store access
fault in reset_osc on about one reload in six. With the lock, 30 reloads
in a row ran clean. The ingest path never holds the lock on entry (it
takes it again in add_delta_to_queue), so this cannot self-deadlock.
@github-actions

Copy link
Copy Markdown
Contributor

🎛️ AMY HW CI (AMYboard bench)

Flashed this PR's AMY (LoadTestChord: 6-voice Juno patch=1, one held note every 2 s) onto the physical AMYboard and measured the smoothed render load as the chord grows — back-to-back with the same sketch built at the PR's merge base, so Δ is this PR's own cost.

✅ PASS — the bench ran the test to completion.

notes held main @ e4881fa this PR Δ
1 978 989 +11
2 1133 1147 +14
3 1702 1719 +17
4 1871 1888 +17
5 2457 2480 +23
6 2595 2612 +17

Full chord settled render μs: 2613 (was 2596, Δ +0.7%) (peak 2619, 39 samples)

⬇️ Artifacts: serial log · load trace · report

Self-hosted bench (amyboardci). FAIL means only that the test could not run — the load values are informational, with no threshold and no audio compare. See tools/arduino_loadsweep/.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant