#include "lodepng_alloc.hpp" #include "esp_heap_caps.h" #include "esp_log.h" #include // LVGL's LodePNG memory optimization // Instead of standard heap allocations which fragment quickly and crash on the // ESP32, we allocate a single massive buffer in PSRAM and just bump a pointer // during encode! static const char *kTagLodeAlloc = "LODE_ALLOC"; // 2MB buffer for LodePNG encoding intermediate state. // A typical 800x480 grayscale PNG should compress to ~50-100KB, but the dynamic // window matching and filtering algorithms need a good amount of scratch space. // We can tune this down to 1MB if 2MB is too aggressive, but PSRAM provides // 8MB. #define LODEPNG_ALLOC_POOL_SIZE (1 * 1024 * 1024) static uint8_t *s_lodepng_pool = nullptr; static size_t s_lodepng_pool_used = 0; void lodepng_allocator_init() { if (s_lodepng_pool != nullptr) return; ESP_LOGI(kTagLodeAlloc, "Allocating %d bytes in PSRAM for LodePNG bump allocator...", LODEPNG_ALLOC_POOL_SIZE); // SPIRAM fallback to internal if someone tests without a PSRAM chip s_lodepng_pool = (uint8_t *)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, MALLOC_CAP_SPIRAM); if (!s_lodepng_pool) { s_lodepng_pool = (uint8_t *)heap_caps_malloc(LODEPNG_ALLOC_POOL_SIZE, MALLOC_CAP_DEFAULT); } if (!s_lodepng_pool) { ESP_LOGE(kTagLodeAlloc, "CRITICAL: Failed to allocate LodePNG PSRAM pool!"); } } void lodepng_allocator_reset() { s_lodepng_pool_used = 0; } void lodepng_allocator_free() { if (s_lodepng_pool) { free(s_lodepng_pool); s_lodepng_pool = nullptr; } s_lodepng_pool_used = 0; } // ---------------------------------------------------- // Custom Allocators injected into lodepng.c // ---------------------------------------------------- // To support realloc properly, we prefix each allocation with an 8-byte header // storing the size. struct AllocHeader { size_t size; }; void *lodepng_custom_malloc(size_t size) { if (!s_lodepng_pool) { ESP_LOGE(kTagLodeAlloc, "lodepng_malloc called before lodepng_allocator_init!"); return nullptr; } // Align size to 8 bytes to avoid unaligned access faults size_t aligned_size = (size + 7) & ~7; size_t total_alloc = sizeof(AllocHeader) + aligned_size; if (s_lodepng_pool_used + total_alloc > LODEPNG_ALLOC_POOL_SIZE) { ESP_LOGE(kTagLodeAlloc, "LodePNG pool exhausted! Requested: %zu, Used: %zu, Total: %d", size, s_lodepng_pool_used, LODEPNG_ALLOC_POOL_SIZE); return nullptr; } // Grab pointer and bump uint8_t *ptr = s_lodepng_pool + s_lodepng_pool_used; s_lodepng_pool_used += total_alloc; // Write header AllocHeader *header = (AllocHeader *)ptr; header->size = size; // We store exact size for realloc memcpy bounds // Return pointer right after header return ptr + sizeof(AllocHeader); } void *lodepng_custom_realloc(void *ptr, size_t new_size) { if (!ptr) { return lodepng_custom_malloc(new_size); } if (new_size == 0) { lodepng_custom_free(ptr); return nullptr; } // Get original header uint8_t *orig_ptr = (uint8_t *)ptr - sizeof(AllocHeader); AllocHeader *header = (AllocHeader *)orig_ptr; size_t old_size = header->size; if (new_size <= old_size) { // Don't shrink to save time, bump allocator can't reclaim it easily anyway. return ptr; } // Let's see if this ptr was the *very last* allocation. // If so, we can just expand it in place! size_t old_aligned_size = (old_size + 7) & ~7; if (orig_ptr + sizeof(AllocHeader) + old_aligned_size == s_lodepng_pool + s_lodepng_pool_used) { // We are at the end! Just bump further! size_t new_aligned_size = (new_size + 7) & ~7; size_t size_diff = new_aligned_size - old_aligned_size; if (s_lodepng_pool_used + size_diff > LODEPNG_ALLOC_POOL_SIZE) { ESP_LOGE(kTagLodeAlloc, "LodePNG pool exhausted during in-place realloc!"); return nullptr; } s_lodepng_pool_used += size_diff; header->size = new_size; return ptr; } // Otherwise, we have to copy into a new block void *new_ptr = lodepng_custom_malloc(new_size); if (new_ptr) { memcpy(new_ptr, ptr, old_size); } return new_ptr; } void lodepng_custom_free(void *ptr) { // No-op! The bump pointer will just reset to 0 once the API endpoint is done! (void)ptr; }