Skip to content

introduce psram-aware malloc functions (backport of upstream #4895)#342

Open
softhack007 wants to merge 14 commits intomdevfrom
dedehai_malloc
Open

introduce psram-aware malloc functions (backport of upstream #4895)#342
softhack007 wants to merge 14 commits intomdevfrom
dedehai_malloc

Conversation

@softhack007
Copy link
Collaborator

@softhack007 softhack007 commented Feb 15, 2026

introduces new memory allocation functions, based on wled#4895 by @DedeHai

  • d_malloc(), d_calloc(), d_malloc_realloc() prefer DRAM (faster access), but fall back to PSRAM if available
  • p_malloc(), p_calloc(), p_malloc_realloc() prefer PSRAM (data not time critical), but fall back to DRAM on boards without PSRAM
  • S3/S2/C3 automatically use "fast RTC Ram" for small data
  • some original functions from upsteam disabled until we have some confidence from testing (performance, possibly causing LEDs flickering)

Summary by CodeRabbit

  • Refactor

    • Introduced a unified, platform-aware memory allocation layer and safer buffer initialization with additional bounds checks; reduced external symbol exposure for some internal string tables.
  • Stability

    • More robust memory handling across features (audio/FFT, presets, mappings, usermods), reducing allocation-related failures and potential crashes.
  • Diagnostics

    • Enhanced runtime memory reporting on ESP32/RTC targets to aid troubleshooting.

DedeHai and others added 5 commits February 15, 2026 18:57
* Improved heap and PSRAM handling

- Segment `allocateData()` uses more elaborate DRAM checking to reduce fragmentation and allow for larger setups to run on low heap
- Segment data allocation fails if minimum contiguous block size runs low to keep the UI working
- Increased `MAX_SEGMENT_DATA` to account for better segment data handling
- Memory allocation functions try to keep enough DRAM for segment data
- Added constant `PSRAM_THRESHOLD` to improve PSARM usage
- Increase MIN_HEAP_SIZE to reduce risk of breaking UI due to low memory for JSON response
- ESP32 makes use of IRAM (no 8bit access) for pixeluffers, freeing up to 50kB of RAM
- Fix to properly get available heap on all platforms: added function `getFreeHeapSize()`
- Bugfix for effects that divide by SEGLEN: don't run FX in service() if segment is not active
-Syntax fix in AR: calloc() uses (numelements, size) as arguments

* Added new functions for allocation and heap checking

- added `allocate_buffer()` function that can be used to allocate large buffers: takes parameters to set preferred ram location, including 32bit accessible RAM on ESP32. Returns null if heap runs low or switches to PSRAM
- getFreeHeapSize() and getContiguousFreeHeap() helper functions for all platforms to correctly report free useable heap
- updated some constants
- updated segment data allocation to free the data if it is large

- replaced "psramsafe" variable with it's #ifdef: BOARD_HAS_PSRAM and made accomodating changes
- added some compile-time checks to handle invalid env. definitions
- updated all allocation functions and some of the logic behind them
- added use of fast RTC-Memory where available
- increased MIN_HEAP_SIZE for all systems (improved stability in tests)
- updated memory calculation in web-UI to account for required segment buffer
- added UI alerts if buffer allocation fails
- made getUsedSegmentData() non-private (used in buffer alloc function)
- changed MAX_SEGMENT_DATA
- added more detailed memory log to DEBUG output
- added debug output to buffer alloc function
* p_malloc() functions always available, to simplify code by avoiding ifdef's
* simplify extern definitions
* always use SPIRAM as fallback
* implement calloc(), instead of relying on malloc()
* we don't (yet) use allocate_buffer()
* remove some commented-out code
@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds DRAM-biased (d_) and PSRAM-preferring (p_) allocation APIs and platform-aware wrappers, replaces direct heap calls across many modules to route allocations through these helpers, exposes allocator declarations publicly, and tightens visibility for several date string constants.

Changes

Cohort / File(s) Summary
Allocator implementation
wled00/util.cpp
Implements d_/p_ allocator family, heap-validation helpers, PSRAM/RTC/DRAM selection logic, and public allocator functions (d_/p_).
Public declarations & flags
wled00/fcn_declare.h, wled00/wled.h, wled00/const.h, wled00/bus_manager.cpp
Adds extern "C" declarations for d_/p_ allocators, inline heap-query helpers, MIN_HEAP_SIZE / PSRAM_THRESHOLD macros, and exposes allocator symbols for linkage.
Core allocator wiring
wled00/wled.h, wled00/util.cpp
PSRAM_Allocator now routes allocate/reallocate/deallocate to p_malloc/p_realloc_malloc/p_free; public extern "C" allocator declarations added.
FX & LED buffers
wled00/FX.h, wled00/FX_fcn.cpp, wled00/FX_2Dfcn.cpp
Replaced malloc/calloc/reallocf/free/new/delete with corresponding d_/p_/p_free/d_realloc_malloc/d_calloc wrappers across segments, LED buffers, mapping tables, and related allocation/deallocation paths.
JSON, file & presets
wled00/src/dependencies/json/AsyncJson-v6.h, wled00/file.cpp, wled00/presets.cpp
Expose allocator symbols to JSON dependency; use d_malloc in Async handler; switch temp/preset JSON buffers to p_malloc/p_free and add runtime psramFound() guards for PSRAM paths.
Bus manager & linkage
wled00/bus_manager.cpp
Adds extern "C" forward declarations for d_/p_ allocator functions to make symbols available in this TU.
Usermods & audio
usermods/artifx/arti.h, usermods/audioreactive/audio_reactive.h, usermods/usermod_v2_rotary_encoder_ui_ALT/...h
Swap malloc/calloc/free to d_/p_ variants (d_malloc/d_calloc/p_malloc/p_free/p_calloc) for program text, FFT buffers, pinkFactors; add zero-init d_calloc for strings and a bounds check for mode strings.
Time/date constants
wled00/src/dependencies/time/DateStrings.cpp
Make month/day PROGMEM string constants and pointer arrays static (internal linkage) and fix preprocessor guard structure.
Miscellaneous updates
wled00/*, usermods/*, wled00/src/dependencies/*
Many files updated with straightforward allocator substitutions (malloc/calloc/realloc/free/new/delete -> d_/p_/d_calloc/p_calloc/p_free/d_realloc_malloc) and small debug/print changes (WLED loop DRAM/RTC reporting).

Sequence Diagram(s)

sequenceDiagram
    participant Module as Caller (FX / JSON / Preset / Usermod)
    participant AllocAPI as Allocator API (d_/p_)
    participant HeapChk as Heap Validator
    participant MemPool as Memory Pools (DRAM / PSRAM / RTC)

    Module->>AllocAPI: request allocate(size, preferences)
    AllocAPI->>HeapChk: check MIN_HEAP_SIZE / contiguous space
    alt PSRAM preferred & available
        AllocAPI->>MemPool: p_malloc / p_calloc / p_realloc_malloc
    else DRAM/RTC fallback
        AllocAPI->>MemPool: d_malloc / d_calloc / d_realloc_malloc
    end
    MemPool-->>AllocAPI: pointer or NULL
    AllocAPI-->>Module: pointer or NULL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hopping through heaps both near and far,
d_ and p_ fetch the memory jar,
old mallocs rest while wrappers lead the way,
Rabbits tidy bytes and guard the day,
🥕 Small hops, big frees — a cosier array.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: introducing PSRAM-aware malloc functions as a backport from upstream PR #4895, which directly aligns with the PR's core objective of adding memory allocation APIs.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into mdev

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dedehai_malloc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h (1)

366-383: ⚠️ Potential issue | 🟠 Major

Use 16‑bit counters for mode indices (palette stays 8‑bit).

modeIndex is uint8_t, so when mode count exceeds 255 it wraps and the new bounds check can still overwrite earlier slots. Switch mode-related indices/arrays to uint16_t (keep palette indices as uint8_t) and add a null guard for modeStrings.

🛠️ Proposed fix
-  const char **modeStrings = (const char **)d_calloc(numModes, sizeof(const char *));
-  uint8_t modeIndex = 0;
+  const char **modeStrings = (const char **)d_calloc(numModes, sizeof(const char *));
+  if (!modeStrings) return nullptr;
+  uint16_t modeIndex = 0;

Based on learnings: In MoonModules/WLED-MM PR #270, only Effect (Mode) IDs are migrated to 16-bit; Palette IDs and counts remain 8-bit. Usermod code should widen mode-related indices/loops to uint16_t while keeping palette-related indices/loops as uint8_t.

🤖 Fix all issues with AI agents
In `@usermods/artifx/arti.h`:
- Around line 2567-2571: The allocation for programText using d_malloc can
return nullptr; before calling programFile.read or dereferencing programText,
check the result of d_malloc and handle failure (e.g., log an error via the
existing error/reporting mechanism and return or exit cleanly). Specifically,
after programText = (char *)d_malloc(programFileSize+1) validate programText !=
nullptr, avoid calling programFile.read((byte *)programText, programFileSize) if
null, and ensure any resources (like programFile) are closed or cleaned up
before bailing; reference programFileSize, programText, d_malloc, and
programFile.read to locate and implement this guard.

In `@usermods/audioreactive/audio_reactive.h`:
- Around line 457-464: The allocation sequence allocates vReal then vImag then
pinkFactors but returns early on failure without freeing any earlier
allocations; update the failure paths in the allocation block so that if vImag
allocation fails you call d_free(vReal) before returning false, and if
pinkFactors allocation fails you call d_free(vReal) and d_free(vImag) (and
p_free(pinkFactors) only if non-null) before returning false; use the existing
d_free and p_free helpers and the same null-check style used elsewhere to avoid
leaks (references: vReal, vImag, pinkFactors, d_calloc, p_calloc, d_free,
p_free).

In
`@usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h`:
- Around line 354-358: The function RotaryEncoderUIUsermod::re_initIndexArray
calls d_calloc and immediately dereferences the returned pointer; add a null
check after the allocation (check if indexes == nullptr) and if allocation
failed return nullptr (or handle the error) before entering the for loop that
writes to indexes, so the loop doesn't dereference a null pointer.

In `@wled00/fcn_declare.h`:
- Line 518: The comment for the macro BFRALLOC_ENFORCE_PSRAM is inaccurate: the
allocation logic in util.cpp treats this flag as "PSRAM-only" and returns
nullptr if PSRAM is not available rather than falling back to DRAM. Update the
macro comment for BFRALLOC_ENFORCE_PSRAM to state that it enforces PSRAM-only
allocation and will fail (return nullptr) when PSRAM is unavailable, or
alternatively change the allocation logic in util.cpp to implement a DRAM
fallback; reference the macro name BFRALLOC_ENFORCE_PSRAM and the allocation
routine in util.cpp that checks this flag when making your change.

In `@wled00/presets.cpp`:
- Around line 80-86: The code allocates tmpRAMbuffer with p_malloc but some
deallocation paths still call free(); update those deallocation sites (e.g., the
temp preset load path) to call p_free(tmpRAMbuffer) instead of
free(tmpRAMbuffer) so the PSRAM allocator pairing is consistent; search for any
other occurrences of free(tmpRAMbuffer) and replace them with p_free to ensure
all allocations via p_malloc are freed with p_free.

In `@wled00/util.cpp`:
- Around line 817-827: The current d_realloc_malloc uses heap_caps_realloc_*
which can transiently allocate extra heap; change it to free the old buffer
before allocating the new one to avoid double-heap spikes: in function
d_realloc_malloc, remove the direct heap_caps_realloc_* path and instead call
d_free(ptr) first and then allocate a new buffer with d_malloc(size) (or the
appropriate heap_caps_malloc variant matching previous caps), validate the
allocation with validateFreeHeap, and return the new buffer (or NULL on
failure); ensure you only free once (use ptr only before allocation) and
preserve the existing validateFreeHeap and fallback behavior previously returned
by heap_caps_realloc_*.
🧹 Nitpick comments (3)
wled00/const.h (1)

531-550: Allow MIN_HEAP_SIZE overrides and remove the redundant fallback.

MIN_HEAP_SIZE is defined unconditionally, then a fallback for the undefined case remains. This blocks build-flag overrides and can trigger macro redefinition warnings. Wrap the new block in #ifndef MIN_HEAP_SIZE and remove (or relocate) the later fallback.

♻️ Suggested refactor
-// minimum heap size required to process web requests: try to keep free heap above this value
-#ifdef ESP8266
-  `#define` MIN_HEAP_SIZE (9*1024)
-#else
-  `#define` MIN_HEAP_SIZE (15*1024) // WLED allocation functions (util.cpp) try to keep this much contiguous heap free for other tasks
-#endif
+// minimum heap size required to process web requests: try to keep free heap above this value
+#ifndef MIN_HEAP_SIZE
+  `#ifdef` ESP8266
+    `#define` MIN_HEAP_SIZE (9*1024)
+  `#else`
+    `#define` MIN_HEAP_SIZE (15*1024) // WLED allocation functions (util.cpp) try to keep this much contiguous heap free for other tasks
+  `#endif`
+#endif
@@
-#if !defined(MIN_HEAP_SIZE)
-#define MIN_HEAP_SIZE 8192
-#endif
wled00/util.cpp (1)

840-845: Remove unreachable return in p_calloc

return buffer; after return validateFreeHeap(buffer); is dead code.

🧹 Cleanup
 void *p_calloc(size_t count, size_t size) {
   // similar to p_malloc bus uses heap_caps_calloc
   void *buffer = heap_caps_calloc_prefer(count, size, 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT);
   return validateFreeHeap(buffer);
-  return buffer;
 }
wled00/FX_fcn.cpp (1)

2704-2712: Prefer free‑then‑alloc for mapping table growth to avoid transient double allocations.
customMappingTable is rebuilt immediately afterward, so preserving the old contents isn’t necessary. Avoiding realloc-style growth reduces peak heap usage on low-memory targets.

♻️ Suggested adjustment
-    if ((size > 0) && (customMappingTable != nullptr)) {
-      customMappingTable = (uint16_t*) d_realloc_malloc(customMappingTable, sizeof(uint16_t) * size);  // reallocf will free memory if it cannot resize
-    }
-    if ((size > 0) && (customMappingTable == nullptr)) { // second try
-      DEBUG_PRINTLN("deserializeMap: trying to get fresh memory block.");
-      customMappingTable = (uint16_t*) d_calloc(size, sizeof(uint16_t));
-      if (customMappingTable == nullptr) { 
-        DEBUG_PRINTLN("deserializeMap: alloc failed!");
-        errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag
-      }
-    }
+    if (size > 0) {
+      if (customMappingTable) { d_free(customMappingTable); customMappingTable = nullptr; }
+      DEBUG_PRINTLN("deserializeMap: trying to get fresh memory block.");
+      customMappingTable = (uint16_t*) d_calloc(size, sizeof(uint16_t));
+      if (customMappingTable == nullptr) { 
+        DEBUG_PRINTLN("deserializeMap: alloc failed!");
+        errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag
+      }
+    }

Based on learnings: “In MoonModules/WLED-MM on ESP32/ESP8266 platforms with limited heap memory, when reallocating buffers … always free the old buffer before allocating the new one.”

Comment on lines 2567 to 2571
programFileSize = programFile.size();
programText = (char *)malloc(programFileSize+1);
programText = (char *)d_malloc(programFileSize+1);
programFile.read((byte *)programText, programFileSize);
programText[programFileSize] = '\0';
#else
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle d_malloc failure before reading the program file

d_malloc can return nullptr (especially with MIN_HEAP_SIZE constraints). The current code immediately reads into programText, which will crash on allocation failure. Please guard and bail out cleanly.

🛠️ Suggested fix
   programFileSize = programFile.size();
   programText = (char *)d_malloc(programFileSize+1);
+  if (programText == nullptr) {
+    ERROR_ARTI("ARTI-FX: Out of memory allocating program text (%u bytes)\n", programFileSize + 1);
+    programFile.close();
+    return false;
+  }
   programFile.read((byte *)programText, programFileSize);
   programText[programFileSize] = '\0';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
programFileSize = programFile.size();
programText = (char *)malloc(programFileSize+1);
programText = (char *)d_malloc(programFileSize+1);
programFile.read((byte *)programText, programFileSize);
programText[programFileSize] = '\0';
#else
programFileSize = programFile.size();
programText = (char *)d_malloc(programFileSize+1);
if (programText == nullptr) {
ERROR_ARTI("ARTI-FX: Out of memory allocating program text (%u bytes)\n", programFileSize + 1);
programFile.close();
return false;
}
programFile.read((byte *)programText, programFileSize);
programText[programFileSize] = '\0';
`#else`
🤖 Prompt for AI Agents
In `@usermods/artifx/arti.h` around lines 2567 - 2571, The allocation for
programText using d_malloc can return nullptr; before calling programFile.read
or dereferencing programText, check the result of d_malloc and handle failure
(e.g., log an error via the existing error/reporting mechanism and return or
exit cleanly). Specifically, after programText = (char
*)d_malloc(programFileSize+1) validate programText != nullptr, avoid calling
programFile.read((byte *)programText, programFileSize) if null, and ensure any
resources (like programFile) are closed or cleaned up before bailing; reference
programFileSize, programText, d_malloc, and programFile.read to locate and
implement this guard.

Comment on lines +457 to 464
if (vReal) d_free(vReal); // should not happen
if (vImag) d_free(vImag); // should not happen
if ((vReal = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) return false; // calloc or die
if ((vImag = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) return false;
#ifdef FFT_MAJORPEAK_HUMAN_EAR
if (pinkFactors) free(pinkFactors);
if ((pinkFactors = (float*) calloc(samplesFFT, sizeof(float))) == nullptr) return false;
if (pinkFactors) p_free(pinkFactors);
if ((pinkFactors = (float*) p_calloc(samplesFFT, sizeof(float))) == nullptr) return false;
#endif
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Free partially allocated FFT buffers on failure.

If vImag or pinkFactors allocation fails, vReal stays allocated and you return false, keeping the heap under pressure and hindering recovery. Free any already-allocated buffers before returning.

🛠️ Proposed fix
-  if ((vImag = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) return false;
+  if ((vImag = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) {
+    d_free(vReal);
+    vReal = nullptr;
+    return false;
+  }
`#ifdef` FFT_MAJORPEAK_HUMAN_EAR
-  if ((pinkFactors = (float*) p_calloc(samplesFFT, sizeof(float))) == nullptr) return false;
+  if ((pinkFactors = (float*) p_calloc(samplesFFT, sizeof(float))) == nullptr) {
+    d_free(vReal);
+    d_free(vImag);
+    vReal = nullptr;
+    vImag = nullptr;
+    return false;
+  }
`#endif`
🤖 Prompt for AI Agents
In `@usermods/audioreactive/audio_reactive.h` around lines 457 - 464, The
allocation sequence allocates vReal then vImag then pinkFactors but returns
early on failure without freeing any earlier allocations; update the failure
paths in the allocation block so that if vImag allocation fails you call
d_free(vReal) before returning false, and if pinkFactors allocation fails you
call d_free(vReal) and d_free(vImag) (and p_free(pinkFactors) only if non-null)
before returning false; use the existing d_free and p_free helpers and the same
null-check style used elsewhere to avoid leaks (references: vReal, vImag,
pinkFactors, d_calloc, p_calloc, d_free, p_free).

Comment on lines 354 to 358
byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) {
byte *indexes = (byte *)malloc(sizeof(byte) * numModes);
byte *indexes = (byte *)d_calloc(numModes, sizeof(byte));
for (byte i = 0; i < numModes; i++) {
indexes[i] = i;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard d_calloc failure before populating the index array.

If d_calloc returns nullptr, the loop dereferences it and crashes. Add an early return on allocation failure.

🛠️ Proposed fix
-  byte *indexes = (byte *)d_calloc(numModes, sizeof(byte));
-  for (byte i = 0; i < numModes; i++) {
+  byte *indexes = (byte *)d_calloc(numModes, sizeof(byte));
+  if (!indexes) return nullptr;
+  for (byte i = 0; i < numModes; i++) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) {
byte *indexes = (byte *)malloc(sizeof(byte) * numModes);
byte *indexes = (byte *)d_calloc(numModes, sizeof(byte));
for (byte i = 0; i < numModes; i++) {
indexes[i] = i;
}
byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) {
byte *indexes = (byte *)d_calloc(numModes, sizeof(byte));
if (!indexes) return nullptr;
for (byte i = 0; i < numModes; i++) {
indexes[i] = i;
}
🤖 Prompt for AI Agents
In `@usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h`
around lines 354 - 358, The function RotaryEncoderUIUsermod::re_initIndexArray
calls d_calloc and immediately dereferences the returned pointer; add a null
check after the allocation (check if indexes == nullptr) and if allocation
failed return nullptr (or handle the error) before entering the for loop that
writes to indexes, so the loop doesn't dereference a null pointer.

#define BFRALLOC_PREFER_DRAM (1 << 1) // prefer DRAM over PSRAM
#define BFRALLOC_ENFORCE_DRAM (1 << 2) // use DRAM only, no PSRAM
#define BFRALLOC_PREFER_PSRAM (1 << 3) // prefer PSRAM over DRAM
#define BFRALLOC_ENFORCE_PSRAM (1 << 4) // use PSRAM if available, otherwise uses DRAM
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align BFRALLOC_ENFORCE_PSRAM comment with behavior

In util.cpp this flag allocates PSRAM-only and returns nullptr if PSRAM is unavailable; the comment says it falls back to DRAM. Please update the comment (or the behavior) to avoid misuse.

🔧 Suggested comment fix
-#define BFRALLOC_ENFORCE_PSRAM   (1 << 4) // use PSRAM if available, otherwise uses DRAM
+#define BFRALLOC_ENFORCE_PSRAM   (1 << 4) // use PSRAM only; returns nullptr if unavailable
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#define BFRALLOC_ENFORCE_PSRAM (1 << 4) // use PSRAM if available, otherwise uses DRAM
`#define` BFRALLOC_ENFORCE_PSRAM (1 << 4) // use PSRAM only; returns nullptr if unavailable
🤖 Prompt for AI Agents
In `@wled00/fcn_declare.h` at line 518, The comment for the macro
BFRALLOC_ENFORCE_PSRAM is inaccurate: the allocation logic in util.cpp treats
this flag as "PSRAM-only" and returns nullptr if PSRAM is not available rather
than falling back to DRAM. Update the macro comment for BFRALLOC_ENFORCE_PSRAM
to state that it enforces PSRAM-only allocation and will fail (return nullptr)
when PSRAM is unavailable, or alternatively change the allocation logic in
util.cpp to implement a DRAM fallback; reference the macro name
BFRALLOC_ENFORCE_PSRAM and the allocation routine in util.cpp that checks this
flag when making your change.

add fallback (d_malloc()) if psram not found (can happen before setup(), or when a BOARD_HAS_PSRAM build runs on boards without PSRAM)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@wled00/util.cpp`:
- Line 797: Fix the minor typo in the comments that read "similar to d_malloc
bus uses" and "similar to p_malloc bus uses" by changing "bus" to "but" in both
occurrences; search for the exact comment strings near the d_malloc/p_malloc
helper implementations (around the comments at the spots corresponding to those
lines) and update them to "similar to d_malloc but uses" and "similar to
p_malloc but uses" respectively so the wording is correct.
- Around line 796-806: The RTC RAM threshold logic in d_calloc incorrectly
compares the per-element size variable to RTC_RAM_THRESHOLD and the contiguous
heap check uses size instead of the total allocation size; update d_calloc to
compute total = count * size (safely checking for overflow) and use total for
the RTC_RAM_THRESHOLD comparison and the getContiguousFreeHeap() comparison
(e.g., compare getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + total), keeping the
existing heap_caps_calloc_prefer and heap_caps_calloc calls but passing count
and size as before; ensure you detect and handle multiplication overflow (return
nullptr or appropriate error) before using total in these checks.
🧹 Nitpick comments (2)
wled00/util.cpp (2)

755-765: ESP32 validateFreeHeap is a no-op — min-heap guard disabled.

The #if 0 means d_malloc/d_calloc on ESP32 will never refuse an allocation based on remaining DRAM, unlike the ESP8266 path. The TODO acknowledges this, but it effectively removes the safety net that prevents the web server from running out of heap. Worth tracking as a follow-up.

Would you like me to open an issue to track re-enabling the ESP32 min-heap validation?


865-909: Disabled allocate_buffer — no review concerns while #if 0.

The guarded code won't compile, so no issues for now. When enabled, note that the validateFreeHeap call on line 902 is outside the #else / PSRAM blocks and would apply even to PSRAM-only allocations — verify that's intended when activating this function.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@wled00/util.cpp`:
- Around line 515-519: In simulateSound(), guard the call to d_malloc for
fftResult: after fftResult = (uint8_t *)d_malloc(...) check for nullptr and
handle it (either return early from simulateSound or allocate/point to a safe
fallback buffer) so fftResult is never dereferenced when allocation fails;
ensure you also avoid initializing or using um_data when fftResult is null and
free/clean up any partially initialized state if you choose an early return.

In `@wled00/wled.cpp`:
- Around line 369-379: The code uses an undefined identifier dram_free when
computing dram32_free; fix by first obtaining the 8-bit DRAM free size into a
variable (e.g., call heap_caps_get_free_size with MALLOC_CAP_8BIT |
MALLOC_CAP_INTERNAL) and subtract that from the 32-bit DRAM total, or else
remove the subtraction and use the correct existing variable; update the
expression that computes dram32_free (the line using heap_caps_get_free_size,
MALLOC_CAP_32BIT, MALLOC_CAP_INTERNAL and dram_free) so it references the
properly declared variable name (or the direct heap_caps_get_free_size call
result) to eliminate the undefined identifier.

Comment on lines 515 to 519
if (!um_data) {
//claim storage for arrays
fftResult = (uint8_t *)malloc(sizeof(uint8_t) * 16);
fftResult = (uint8_t *)d_malloc(sizeof(uint8_t) * 16);

// initialize um_data pointer structure
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against d_malloc() returning nullptr in simulateSound().

d_malloc() can return nullptr when MIN_HEAP_SIZE is enforced; fftResult is then dereferenced unconditionally. Add a safe fallback or early return to avoid a crash.

🔧 Suggested fix
-    fftResult = (uint8_t *)d_malloc(sizeof(uint8_t) * 16);
+    fftResult = (uint8_t *)d_malloc(sizeof(uint8_t) * 16);
+    if (!fftResult) {
+      static uint8_t fftResultFallback[16] = {0};
+      fftResult = fftResultFallback;
+    }
🤖 Prompt for AI Agents
In `@wled00/util.cpp` around lines 515 - 519, In simulateSound(), guard the call
to d_malloc for fftResult: after fftResult = (uint8_t *)d_malloc(...) check for
nullptr and handle it (either return early from simulateSound or allocate/point
to a safe fallback buffer) so fftResult is never dereferenced when allocation
fails; ensure you also avoid initializing or using um_data when fftResult is
null and free/clean up any partially initialized state if you choose an early
return.

Comment on lines +369 to +379
#if defined(CONFIG_IDF_TARGET_ESP32)
// 32-bit DRAM (not byte accessible, only available on ESP32)
size_t dram32_free = heap_caps_get_free_size(MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL) - dram_free; // returns all 32bit DRAM, subtract 8bit DRAM
//size_t dram32_largest = heap_caps_get_largest_free_block(MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL); // returns largest DRAM block -> not useful
DEBUG_PRINTF_P(PSTR("DRAM 32-bit: Free: %7u bytes | Largest block: N/A\n"), dram32_free);
#else
// Fast RTC Memory (not available on ESP32)
size_t rtcram_free = heap_caps_get_free_size(MALLOC_CAP_RTCRAM);
size_t rtcram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_RTCRAM);
DEBUG_PRINTF_P(PSTR("RTC RAM: Free: %7u bytes | Largest block: %7u bytes\n"), rtcram_free, rtcram_largest);
#endif
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "dram_free" -B 5 -A 5 wled00/wled.cpp

Repository: MoonModules/WLED-MM

Length of output: 900


🏁 Script executed:

rg -n "dram_free\s*=" wled00/wled.cpp | head -20

Repository: MoonModules/WLED-MM

Length of output: 45


🏁 Script executed:

sed -n '300,380p' wled00/wled.cpp | cat -n

Repository: MoonModules/WLED-MM

Length of output: 4492


🏁 Script executed:

sed -n '1,370p' wled00/wled.cpp | grep -n "dram_free" -B 5 -A 2

Repository: MoonModules/WLED-MM

Length of output: 45


🏁 Script executed:

rg -n "dram_free|dram.*free" wled00/wled.cpp

Repository: MoonModules/WLED-MM

Length of output: 471


🏁 Script executed:

rg -n "dram_free" wled00/ --include="*.h" --include="*.cpp"

Repository: MoonModules/WLED-MM

Length of output: 504


🏁 Script executed:

fd -e h -e cpp . wled00/ | xargs rg "dram_free"

Repository: MoonModules/WLED-MM

Length of output: 228


🏁 Script executed:

sed -n '340,375p' wled00/wled.cpp

Repository: MoonModules/WLED-MM

Length of output: 2046


🏁 Script executed:

sed -n '300,400p' wled00/wled.cpp | cat -n

Repository: MoonModules/WLED-MM

Length of output: 5590


🏁 Script executed:

sed -n '350,385p' wled00/wled.cpp | cat -n

Repository: MoonModules/WLED-MM

Length of output: 2556


🏁 Script executed:

sed -n '1,360p' wled00/wled.cpp | grep -n "dram"

Repository: MoonModules/WLED-MM

Length of output: 45


🏁 Script executed:

rg -n "size_t.*free" wled00/wled.cpp

Repository: MoonModules/WLED-MM

Length of output: 532


🏁 Script executed:

sed -n '365,385p' wled00/wled.cpp

Repository: MoonModules/WLED-MM

Length of output: 1700


dram_free is undefined and will cause a compilation error.

Line 371 references dram_free in the calculation but this variable is never declared anywhere in the file. This will fail to compile with an "undefined identifier" error. Either declare dram_free before line 371 (likely by calculating it from a prior heap_caps_get_free_size call for 8-bit DRAM) or replace it with the correct variable name.

🤖 Prompt for AI Agents
In `@wled00/wled.cpp` around lines 369 - 379, The code uses an undefined
identifier dram_free when computing dram32_free; fix by first obtaining the
8-bit DRAM free size into a variable (e.g., call heap_caps_get_free_size with
MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL) and subtract that from the 32-bit DRAM
total, or else remove the subtraction and use the correct existing variable;
update the expression that computes dram32_free (the line using
heap_caps_get_free_size, MALLOC_CAP_32BIT, MALLOC_CAP_INTERNAL and dram_free) so
it references the properly declared variable name (or the direct
heap_caps_get_free_size call result) to eliminate the undefined identifier.

* bugfix for p_malloc compilation guard
* enable validateFreeHeap
* comments
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
wled00/util.cpp (3)

796-806: size * count can silently overflow on 32-bit targets.

Line 800 computes size * count without an overflow guard. On ESP32 (32-bit size_t), large inputs could wrap to a small value, passing the threshold check and requesting an undersized allocation. Consider adding a simple overflow check.

🛡️ Proposed overflow guard
 void *d_calloc(size_t count, size_t size) {
   // similar to d_malloc but uses heap_caps_calloc
   void *buffer = nullptr;
+  size_t total = count * size;
+  if (count != 0 && total / count != size) return nullptr; // overflow
   `#if` !defined(CONFIG_IDF_TARGET_ESP32)
-  if ((size * count) <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + (size * count))
+  if (total <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + total)
     buffer = heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);

755-765: #if 1 // WLEDMM TODO need to think about this — resolve or document the TODO.

This conditional guard is permanently enabled and the TODO suggests uncertainty about the approach. If the logic is validated, remove the #if 1 wrapper and the TODO comment; otherwise, document the known risk (e.g., TOCTOU between allocation and the free-heap check).


835-855: p_malloc and p_calloc enforce MIN_HEAP_SIZE via validateFreeHeap, but p_realloc_malloc does not.

p_realloc_malloc (line 849) skips the validateFreeHeap call after a successful realloc, unlike the d_realloc_malloc path and the p_malloc/p_calloc siblings. If the realloc returns a DRAM buffer (fallback), it could push DRAM below MIN_HEAP_SIZE unchecked.

That said, adding validateFreeHeap here would reintroduce the same double-free risk discussed above. If you adopt the free-then-malloc approach for d_realloc_malloc, consider doing the same here for consistency:

♻️ Proposed consistency fix
 void *p_realloc_malloc(void *ptr, size_t size) {
-  if (!psramFound()) return d_realloc_malloc(ptr, size);
-  void *buffer = heap_caps_realloc_prefer(ptr, size, 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT);
-  if (buffer) return buffer; // realloc successful
-  p_free(ptr); // free old buffer if realloc failed
-  return p_malloc(size); // fallback to malloc
+  p_free(ptr);
+  return p_malloc(size);
 }

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