Skip to content

Route fountain-decoder allocations to PSRAM on ESP32#9

Closed
kdmukAI-bot wants to merge 1 commit into
odudex:mainfrom
kdmukAI-bot:feat/esp-psram-allocator
Closed

Route fountain-decoder allocations to PSRAM on ESP32#9
kdmukAI-bot wants to merge 1 commit into
odudex:mainfrom
kdmukAI-bot:feat/esp-psram-allocator

Conversation

@kdmukAI-bot

Copy link
Copy Markdown
Contributor

On an ESP32 with external SPIRAM, ESP-IDF's malloc() forces every allocation
at or below CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL (16 KB by default) into
internal RAM. The fountain decoder's working set is many small, variably-sized
allocations — up to MAX_MIXED_PARTS part buffers plus per-part fragments,
allocated and freed as parts arrive and are XOR-reduced. Kept in internal RAM
they have two compounding effects:

  • Steady growth of the internal footprint as mixed parts accumulate.
  • Fragmentation of the internal heap from the churn of many differently
    sized small allocations, so the largest contiguous free block shrinks well
    below total free.

Fragmentation is the more damaging effect. The failure we hit was not the decode
itself running out of memory, but a later allocation that needs a contiguous
internal block failing even with ample free RAM — specifically being unable to
re-initialize another internal-RAM subsystem after a scan (re-launching the
camera, whose driver needs a contiguous internal buffer/stack that the
fragmented heap could no longer supply).

This routes the shared utility allocators to PSRAM:

  • safe_malloc, safe_malloc_uninit, safe_realloc allocate via
    heap_caps_malloc / heap_caps_realloc(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT),
    falling back to internal RAM when PSRAM is absent.
  • safe_strdup inherits PSRAM through safe_malloc.
  • Everything is behind #ifdef ESP_PLATFORM; host/desktop builds are unchanged.

Moving the churn off the internal heap removes both the internal consumption and
the fragmentation it caused. These are plain CPU-accessed byte buffers, so cached
PSRAM is appropriate; the fountain XOR/copy work over them is sequential and
cache-friendly. free() continues to work on heap_caps allocations.

Verified on an ESP32-P4: a multi-part UR round-trips byte-exact and the fountain
working set no longer draws down or fragments internal RAM.

On an ESP32 with external SPIRAM, ESP-IDF's malloc() forces every allocation
at or below CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL (16 KB by default) into scarce
internal RAM. The fountain decoder's working set is many small, variably-sized
allocations — up to MAX_MIXED_PARTS part buffers plus per-part fragments,
allocated and freed as parts arrive and are XOR-reduced. Kept in internal RAM
they steadily grow the footprint and, worse, fragment the heap: the largest
contiguous free block shrinks well below total free.

The damaging symptom is not the decode itself running out of memory, but a
later allocation that needs a contiguous internal block failing even with ample
free RAM — e.g. being unable to re-initialize another internal-RAM subsystem
after a scan (re-launching the camera, whose driver needs a contiguous internal
buffer/stack the fragmented heap can no longer supply).

Route the shared utility allocators to PSRAM:
- safe_malloc, safe_malloc_uninit, safe_realloc allocate via heap_caps_malloc /
  heap_caps_realloc(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT), falling back to
  internal RAM when no PSRAM is present.
- safe_strdup inherits PSRAM through safe_malloc.
- All guarded by ESP_PLATFORM; host/desktop builds keep plain malloc/realloc.

Moving the churn off the internal heap removes both the internal consumption
and the fragmentation it caused. These are plain CPU-accessed byte buffers, so
cached PSRAM is appropriate and free() continues to work on heap_caps allocations.

Co-Authored-By: kdmukai <934746+kdmukai@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kdmukAI-bot kdmukAI-bot force-pushed the feat/esp-psram-allocator branch from d3a58e1 to 8758b00 Compare July 11, 2026 15:20
kdmukai added a commit to kdmukai/seedsigner-micropython-builder that referenced this pull request Jul 11, 2026
…ix + board_common

Wire the P4-35 partition-mode camera preview through camera_scanner /
camera_entropy (opt-in BOARD_CAMERA_PARTITION_MODE; live gutter progress
scaffolding). Bump board_common (partition preview + zero-copy SPI stripe blit)
and pin deps/cUR to the PSRAM-allocator branch: the fountain decoder's many
small allocations are routed to PSRAM (upstream odudex/cUR#9) so a long
animated-UR scan no longer grows/fragments internal RAM and blocks a later
contiguous internal alloc (e.g. re-launching the camera).

Adds docs/knowledge notes for the SPI stripe DMA-bounce fragmentation root
cause and the camera-QR internal-RAM starvation follow-on.

Co-Authored-By: kdmukai <934746+kdmukai@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@odudex

odudex commented Jul 11, 2026

Copy link
Copy Markdown
Owner

The commit from this PR was cherry-picked.
In follow-up commits, I increased PSRAM allocation to cover other buffers and added a Kconfig opt-out option for PSRAM allocation routing.

@odudex odudex closed this Jul 11, 2026
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.

2 participants