Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 30 additions & 27 deletions ports/micropython/src/lv_mem_core_micropython.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,44 @@
#include <py/mpstate.h>
#include <string.h>

// ESP32: LVGL's allocations come from TLSF pools (ESP-IDF's multi_heap) kept
// inside GC-heap blocks, not from gc_alloc() one by one.
// LVGL's allocations come from TLSF pools kept inside GC-heap blocks, not
// from gc_alloc() one by one, when the build sets LVMP_POOL_TLSF to 1
// (lvmp_pool.h).
//
// gc_alloc() finds room by walking the allocation table from the first free
// block, and LVGL's thousands of small, long-lived blocks leave the table
// full of one-block holes. On an ESP32-S3 with its heap in PSRAM a 64-byte
// allocation took ~90 us and a 2 KB one ~1.3 ms, and creating one LVGL
// widget ~1-2.5 ms: a 30-row list took 2 s to build (Waveshare LCD-7,
// 2026-09-25). multi_heap allocates in O(1).
// 2026-09-25). TLSF allocates in O(1).
//
// The pools are ordinary GC blocks, reachable through a root pointer, so the
// collector still scans every byte of LVGL's memory: a Python wrapper or
// callback that only lv_obj_t.user_data refers to stays alive, as with
// gc_alloc(). Allocations of LVMP_POOL_BIG bytes or more, and anything that
// does not fit, still go to gc_alloc().
//
// Other ports: the same pools on LVGL's own TLSF (lv_tlsf.c, built for this in
// lvmp_tlsf.c) when the board sets LVMP_POOL_TLSF=1. STM32H743 with a 28 MB
// GC heap in SDRAM: a full-screen refresh of 48 buttons took 54 ms with a
// fresh heap and 575 ms once 2 MB of bytearrays sat behind a free block, every
// LVGL allocation walking the table past them (mpy-lvgl-boards, 2026-11-02).
// The allocator is LVGL's own TLSF (lv_tlsf.c, built for this in lvmp_tlsf.c).
// STM32H743 with a 28 MB GC heap in SDRAM: a full-screen refresh of 48 buttons
// took 54 ms with a fresh heap and 575 ms once 2 MB of bytearrays sat behind a
// free block, every LVGL allocation walking the table past them
// (mpy-lvgl-boards, 2026-11-02).
#include "lvmp_pool.h"

#if LVMP_POOL && defined(ESP_PLATFORM)
#include "multi_heap.h"
#elif LVMP_POOL
#if LVMP_POOL
// lv_tlsf.h declares these only with LV_STDLIB_BUILTIN.
typedef void * lv_tlsf_t;
lv_tlsf_t lv_tlsf_create_with_pool(void * mem, size_t bytes);
void * lv_tlsf_malloc(lv_tlsf_t tlsf, size_t bytes);
void * lv_tlsf_realloc(lv_tlsf_t tlsf, void * ptr, size_t size);
size_t lv_tlsf_free(lv_tlsf_t tlsf, const void * ptr);
size_t lv_tlsf_block_size(void * ptr);
typedef lv_tlsf_t multi_heap_handle_t;
#define multi_heap_register(mem, bytes) lv_tlsf_create_with_pool((mem), (bytes))
#define multi_heap_malloc lv_tlsf_malloc
#define multi_heap_realloc lv_tlsf_realloc
#define multi_heap_free(h, p) ((void)lv_tlsf_free((h), (p)))
#define multi_heap_get_allocated_size(h, p) lv_tlsf_block_size(p)
typedef lv_tlsf_t lvmp_heap_t;
#define lvmp_heap_register(mem, bytes) lv_tlsf_create_with_pool((mem), (bytes))
#define lvmp_heap_malloc lv_tlsf_malloc
#define lvmp_heap_realloc lv_tlsf_realloc
#define lvmp_heap_free(h, p) ((void)lv_tlsf_free((h), (p)))
#define lvmp_heap_size(h, p) lv_tlsf_block_size(p)
#endif

#if LVMP_POOL
Expand All @@ -60,7 +59,7 @@ typedef lv_tlsf_t multi_heap_handle_t;
// them) by the root pointer lvmp_pool_mem, registered in lvgl_micropython_build.c:
// make builds do not scan this file (SRC_USERMOD_LIB_C) for root pointers.

static multi_heap_handle_t lvmp_heaps[LVMP_POOL_MAX];
static lvmp_heap_t lvmp_heaps[LVMP_POOL_MAX];
static uint8_t *lvmp_start[LVMP_POOL_MAX];
static uint8_t *lvmp_end[LVMP_POOL_MAX];
static int lvmp_n;
Expand Down Expand Up @@ -91,19 +90,23 @@ static bool lvmp_pool_add(void)
if (lvmp_n >= LVMP_POOL_MAX) {
return false;
}
void *mem = gc_alloc(LVMP_POOL_BYTES, 0);
size_t bytes = (size_t)LVMP_POOL_FIRST << lvmp_n;
if (bytes > LVMP_POOL_BYTES) {
bytes = LVMP_POOL_BYTES;
}
void *mem = gc_alloc(bytes, 0);
if (mem == NULL) {
return false;
}
multi_heap_handle_t h = multi_heap_register(mem, LVMP_POOL_BYTES);
lvmp_heap_t h = lvmp_heap_register(mem, bytes);
if (h == NULL) {
gc_free(mem);
return false;
}
MP_STATE_VM(lvmp_pool_mem)[lvmp_n] = mem;
lvmp_heaps[lvmp_n] = h;
lvmp_start[lvmp_n] = mem;
lvmp_end[lvmp_n] = (uint8_t *)mem + LVMP_POOL_BYTES;
lvmp_end[lvmp_n] = (uint8_t *)mem + bytes;
lvmp_n++;
return true;
}
Expand All @@ -114,13 +117,13 @@ static void *lvmp_pool_malloc(size_t size)
return NULL;
}
for (int i = 0; i < lvmp_n; i++) {
void *p = multi_heap_malloc(lvmp_heaps[i], size);
void *p = lvmp_heap_malloc(lvmp_heaps[i], size);
if (p != NULL) {
return p;
}
}
if (lvmp_pool_add()) {
return multi_heap_malloc(lvmp_heaps[lvmp_n - 1], size);
return lvmp_heap_malloc(lvmp_heaps[lvmp_n - 1], size);
}
return NULL;
}
Expand Down Expand Up @@ -208,16 +211,16 @@ void * lv_realloc_core(void * p, size_t new_size)
#if LVMP_POOL
int pool = p != NULL ? lvmp_pool_of(p) : -1;
if (pool >= 0) {
void *q = multi_heap_realloc(lvmp_heaps[pool], p, new_size);
void *q = lvmp_heap_realloc(lvmp_heaps[pool], p, new_size);
if (q != NULL || new_size == 0) {
return q;
}
// Did not fit in its pool: move it (another pool, or the GC heap).
size_t old_size = multi_heap_get_allocated_size(lvmp_heaps[pool], p);
size_t old_size = lvmp_heap_size(lvmp_heaps[pool], p);
q = lv_malloc_core(new_size);
if (q != NULL) {
memcpy(q, p, old_size < new_size ? old_size : new_size);
multi_heap_free(lvmp_heaps[pool], p);
lvmp_heap_free(lvmp_heaps[pool], p);
}
return q;
}
Expand All @@ -240,7 +243,7 @@ void lv_free_core(void * p)
#if LVMP_POOL
int pool = p != NULL ? lvmp_pool_of(p) : -1;
if (pool >= 0) {
multi_heap_free(lvmp_heaps[pool], p);
lvmp_heap_free(lvmp_heaps[pool], p);
return;
}
#endif
Expand Down
29 changes: 20 additions & 9 deletions ports/micropython/src/lvmp_pool.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,28 @@

#include "py/mpconfig.h"

// ESP32: ESP-IDF's multi_heap. Other ports: LVGL's TLSF, when the board sets
// LVMP_POOL_TLSF=1.
#if defined(ESP_PLATFORM) && MICROPY_MALLOC_USES_ALLOCATED_SIZE
#define LVMP_POOL (1)
#elif defined(LVMP_POOL_TLSF) && LVMP_POOL_TLSF
#define LVMP_POOL (1)
#else
#define LVMP_POOL (0)
// Off unless a build sets LVMP_POOL_TLSF to 1, on every port. Set it where the
// root-pointer scan sees it too: in the board's mpconfigboard.h, or in
// CFLAGS_EXTRA on a make port. A definition that reaches only the compiler
// (on the esp32 port, a CMake target_compile_definitions) leaves the pools'
// root pointer unregistered and the build fails.
//
// The pools trade collection time for allocation time: gc.collect() scans
// them whole, used or not. On an ESP32-P4 with 31 MB of GC heap in PSRAM, a
// collection on a fresh heap took 6.1 ms with them against 2.9 ms without,
// while creating and deleting 50 labels on a fragmented heap took 14 ms with
// them against 43-49 ms without.
#ifndef LVMP_POOL_TLSF
#define LVMP_POOL_TLSF (0)
#endif
#define LVMP_POOL (LVMP_POOL_TLSF)

#define LVMP_POOL_MAX (8)
// The first pool is small and each new one doubles, up to LVMP_POOL_BYTES:
// gc.collect() scans a pool whole, used or not, so a pool much larger than
// what LVGL keeps in it costs every collection. 32 + 64 + 128 + 7 x 256 KB,
// about 2 MB, before allocations go back to gc_alloc().
#define LVMP_POOL_MAX (10)
#define LVMP_POOL_FIRST (32 * 1024)
#define LVMP_POOL_BYTES (256 * 1024)
#define LVMP_POOL_BIG (32 * 1024)

Expand Down
6 changes: 3 additions & 3 deletions ports/micropython/src/lvmp_tlsf.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
/**
* @file lvmp_tlsf.c
*
* LVGL's TLSF allocator for the LVMP pools of lv_mem_core_micropython.c on
* ports without ESP-IDF's multi_heap (LVMP_POOL_TLSF=1). LVGL compiles
* LVGL's TLSF allocator for the LVMP pools of lv_mem_core_micropython.c
* (LVMP_POOL_TLSF, see lvmp_pool.h). LVGL compiles
* lv_tlsf.c only with LV_STDLIB_BUILTIN, so it is built here, in a translation
* unit of its own, with that setting: nothing else in LVGL changes.
*/
#include "lvgl/src/lv_conf_internal.h"
#if LV_USE_STDLIB_MALLOC == LV_STDLIB_MICROPYTHON_OVERRIDE && defined(LVMP_POOL_TLSF) && LVMP_POOL_TLSF
#include "lvmp_pool.h"
#if LV_USE_STDLIB_MALLOC == LV_STDLIB_MICROPYTHON_OVERRIDE && LVMP_POOL
#undef LV_USE_STDLIB_MALLOC
#define LV_USE_STDLIB_MALLOC LV_STDLIB_BUILTIN
// lv_tlsf.c sizes its tables for LV_MEM_SIZE + LV_MEM_POOL_EXPAND_SIZE, which
Expand Down
Loading