From 8758b00f5b1bd39d024ec977d3a4ad0866cf6a8e Mon Sep 17 00:00:00 2001 From: kdmukAI-bot <263064613+kdmukAI-bot@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:08:43 -0500 Subject: [PATCH] feat(esp): route allocations to PSRAM on ESP32 with external SPIRAM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/utils.c | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/utils.c b/src/utils.c index 4afeee4..cb46b42 100644 --- a/src/utils.c +++ b/src/utils.c @@ -15,6 +15,39 @@ #include #include +/* Allocator routing. On an ESP32 with external SPIRAM, the fountain decoder's + * working set — up to MAX_MIXED_PARTS part buffers plus per-part fragments, + * allocated and freed as parts arrive and are XOR-reduced — is many small, + * variably-sized allocations. ESP-IDF's default malloc() forces allocations at + * or below CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL (16 KB by default) into scarce + * INTERNAL RAM, where 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. re-initializing 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). These are plain CPU-accessed + * byte buffers, so route them to PSRAM instead (falling back to internal RAM if + * no PSRAM is present). Off-device this is a no-op. */ +#ifdef ESP_PLATFORM +#include "esp_heap_caps.h" +static void *ur_heap_malloc(size_t size) { + void *p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + return p ? p : heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); +} +static void *ur_heap_realloc(void *ptr, size_t size) { + void *p = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + return p ? p + : heap_caps_realloc(ptr, size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); +} +#define UR_MALLOC(sz) ur_heap_malloc(sz) +#define UR_REALLOC(p, sz) ur_heap_realloc((p), (sz)) +#else +#define UR_MALLOC(sz) malloc(sz) +#define UR_REALLOC(p, sz) realloc((p), (sz)) +#endif + bool str_has_prefix(const char *str, const char *prefix) { if (!str || !prefix) return false; @@ -209,7 +242,7 @@ void free_string_array(char **strings, size_t count) { void *safe_malloc(size_t size) { if (size == 0) return NULL; - void *ptr = malloc(size); + void *ptr = UR_MALLOC(size); if (ptr) { memset(ptr, 0, size); } @@ -219,10 +252,10 @@ void *safe_malloc(size_t size) { void *safe_malloc_uninit(size_t size) { if (size == 0) return NULL; - return malloc(size); + return UR_MALLOC(size); } -void *safe_realloc(void *ptr, size_t size) { return realloc(ptr, size); } +void *safe_realloc(void *ptr, size_t size) { return UR_REALLOC(ptr, size); } char *safe_strdup(const char *str) { if (!str)