Skip to content

Add ram_caps_oscs: separate memory caps for the per-osc state arena - #1107

Merged
dpwe merged 1 commit into
shorepine:mainfrom
rt-rtos:ram-caps-oscs
Aug 17, 2026
Merged

Add ram_caps_oscs: separate memory caps for the per-osc state arena#1107
dpwe merged 1 commit into
shorepine:mainfrom
rt-rtos:ram-caps-oscs

Conversation

@rt-rtos

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

Copy link
Copy Markdown
Contributor

Summary

alloc_osc() draws the per-osc synthinfo/mod_synthinfo/breakpoint
blocks from ram_caps_events, so the arena and the delta pool are
welded to one placement despite opposite profiles: the delta pool is
walked at every ingest and every block and rewards the fastest RAM
available, while osc state is touched at control rate and is the larger
of the two by an order of magnitude on a busy scene. An app that steers
ram_caps_events to internal RAM for the delta pool's sake - my
firmware does exactly this - drags the whole arena with it; the default
ESP config (both on SPIRAM) welds them in the other direction. Freeing
released oscs (#1106) shrinks the arena's lifetime, but a
live heavy scene still parks its working set wherever this one knob
points, and the two purposes deserve separate answers.

This adds ram_caps_oscs next to the existing per-purpose caps
(ram_caps_synth, ram_caps_delay, ...). It defaults to
ram_caps_events, so existing configs behave identically; targets with
tiered memory can steer the arena separately. A config that overrides
ram_caps_events after amy_default_config() and wants the arena to
follow it now sets both fields.

Numbers (ESP32-S3, internal SRAM vs PSRAM tiers)

Each default-sized osc costs 560 B (synthinfo 372 + mod_synthinfo 60 +
2x DEFAULT_NUM_BREAKPOINTS breakpoint arrays at 8 B each; more once a
patch grows a breakpoint set). The budget math is the whole argument:
filling the default pool (max_oscs = 250) costs 250 x 560 B = ~137 KB.
Welded to an internal-RAM delta pool, that has to fit in whatever
internal SRAM the app has left - my firmware had ~56 KB free, a hard
ceiling of ~100 oscs, 40% of the pool it nominally configured. With the
arena on PSRAM the same 137 KB is under 7% of even a 2 MB part, and the
ceiling goes back to being max_oscs itself.

With the arena on the delta pool's caps
(internal RAM), one 4-voice apply of the 25-osc-per-voice piano.h
patch (100 oscs) consumed ~46 KB of internal SRAM - nearly my board's
whole free pool. Pointing ram_caps_oscs at PSRAM freed ~55 KB of
internal at boot (static synths) and cut the piano apply to the ~10 KB
that is genuinely delta-pool growth; render load was unchanged (per-block
state reads from PSRAM are cache-friendly at control rate; the per-sample
buffers stay wherever ram_caps_block/ram_caps_fbl point). The
#1106 churn soaks add sustained evidence with the arena entirely
PSRAM-resident: 30 minutes of per-block render timing at a 1510 us mean
with first-100 and last-100 sample means identical (fixed scene), and
-0.2% render-mean drift between run halves under 90,000 randomized
configure/play/release ops (varying scene).

Notes for reviewers

The breakpoint-growth realloc in ensure_osc_allocd() allocates the
replacement block through alloc_osc(), so it picks up the same caps and
an osc's state never migrates between tiers over its lifetime.

alloc_osc() draws the per-osc synthinfo/mod_synthinfo/breakpoint blocks
from ram_caps_events. On MCUs those caps are usually chosen for the
delta pool - fast internal RAM, since deltas are touched at every ingest
and every block - and the osc arena inherits that placement despite a
different profile: it grows lazily to the high-water mark of osc usage,
never shrinks until amy_reset_oscs(), and is touched at control rate.
Each default-sized osc costs 560 bytes, so a 25-osc-per-voice patch
grows the arena by tens of KB with no way back short of a full reset.

ram_caps_oscs defaults to ram_caps_events, so existing configs behave
identically; targets with tiered memory can steer the arena separately.
On an ESP32-S3 build, pointing it at PSRAM freed ~55 KB of internal
SRAM at boot with render load unchanged (control-rate reads are
cache-friendly; per-sample buffers stay on ram_caps_block/ram_caps_fbl).
@dpwe
dpwe merged commit d97d230 into shorepine:main Aug 17, 2026
9 of 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#1326

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