Skip to content

Join the second-core render with a dedicated semaphore - #1137

Merged
dpwe merged 1 commit into
shorepine:mainfrom
rt-rtos:esp-render-done-sem
Aug 24, 2026
Merged

Join the second-core render with a dedicated semaphore#1137
dpwe merged 1 commit into
shorepine:mainfrom
rt-rtos:esp-render-done-sem

Conversation

@rt-rtos

@rt-rtos rt-rtos commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

esp_render_on_cores() joins the second-core worker with a plain task
notification on the calling task's default notification index. That counter is
not private to the join: the calling task belongs to the app (with
multithread = 0 it is whatever task runs amy_update()), and pacing such a
task with task notifications is idiomatic FreeRTOS - a timer ISR giving one
notification per audio block, for example. When a tick lands inside the join
window, the join returns on the tick instead of the worker: the caller runs
amy_fill_buffer() while the worker is still writing fbl[1] and combines a
half-written buffer. The worker's real "done" give then miscounts the app's
next wait, desyncing its clock. It presents as intermittent, load-dependent
garbling that only appears in multicore configs.

This replaces the caller-side join with a dedicated binary semaphore, which
nothing else can signal, and retires amy_render_task_done_handle. The worker
kick keeps its notification: the worker task is AMY-internal and nothing else
notifies it.

Indexed notifications (xTaskNotifyGiveIndexed on a non-zero index) would
also work but require configTASK_NOTIFICATION_ARRAY_ENTRIES > 1, which the
supported cores don't all guarantee; the semaphore is portable.

Verification

A/B on ESP32-P4 (ESP-IDF 6.1, multicore=1, multithread=0), with the
amy_update() task paced by a GPTimer ISR giving one default-index task
notification per block - the pacing shape described above. To make the
overlap deterministic, the second-core render window was stretched past the
5.33 ms block period so every join spans a tick; a worker-busy flag checked
at join return counts early releases.

  • Unpatched join: 23,929 early releases in a 2-minute run (~180/s,
    essentially every block), the first within 3 s of boot, with the expected
    knock-on desync as the join consumes the app's pacing ticks.
  • With this patch: zero early releases; a separate 17-minute soak
    (194,000 joins) ran with no deadlock and no missed completion.

Originally observed on ESP32-S3 with the same GPTimer pacing: intermittent,
load-dependent dual-core corruption that stopped once the join no longer
shared the task's notification counter. Compile-verified on ESP32-S3
(ESP-IDF 6.0.2) as well.

esp_render_on_cores() joined the worker with a plain task notification
on the calling task's default index.  Any app that also paces that
task with task notifications - a timer-driven block clock notifying
the task that runs amy_update() is the natural shape - shares that
counter: a tick landing inside the join window releases the caller
before the worker finishes, and the caller combines a half-written
second-core buffer.  The result is intermittent, load-dependent
corruption in multicore configs.

Replace the caller-side join with a dedicated binary semaphore that
nothing else can signal, and retire amy_render_task_done_handle.  The
worker kick keeps its notification; the worker task is AMY-internal
and nothing else notifies it.

Seen on hardware (ESP32-S3): a GPTimer block clock notifying the
render task produced intermittent dual-core-only garbling that
stopped once the join no longer shared the notification counter.
@dpwe
dpwe merged commit c4c3df7 into shorepine:main Aug 24, 2026
12 checks passed
@bwhitman

Copy link
Copy Markdown
Collaborator

⛓️ tulipcc integration PR opened

This merge was pinned into tulipcc for full-system CI: shorepine/tulipcc#1337

Test it there and merge that PR to move tulipcc onto this AMY.

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.

3 participants