Skip to main content

Memory

Everything the engine needs is allocated before the first frame, sized by compile-time flags, and nothing is allocated while rendering. That makes a board's memory budget a list of numbers you choose rather than a thing you discover under load. This page is the list.

The engine's pools​

Set these as CMake options before the engine is added (or at the ESP-IDF component level). The defaults are desktop-sized.

FlagDefaultWhat it sizes
ERUI_MAX_NODES512The scene-graph node pool. Every <View>, <Text> and shape is one node, and a hidden page keeps its nodes
ERUI_SCRATCH_WIDTH, ERUI_SCRATCH_HEIGHT240, 240The offscreen buffer for opacity groups, transforms and shadows: the largest node that can fade or rotate. W × H × 4 bytes per strip
ERUI_MAX_OPACITY_DEPTH4Nested translucent groups, one strip each
ERUI_SCRATCH_BAND_HEIGHTERUI_SCRATCH_HEIGHTShrink to render tall fades in more, smaller passes
ERUI_TRANSFORM_WIDTH, ERUI_TRANSFORM_HEIGHTscratch sizeThe transform source, the one buffer that cannot be banded; decouple it when strips are screen-wide but only small widgets rotate
ERUI_FADE_CACHE_WIDTH, ERUI_FADE_CACHE_HEIGHT0 (off)Caches a translucent subtree during a pure opacity animation, roughly doubling fade frame rates; put it in external RAM
ER_DAMAGE_RECTS_MAX16Disjoint dirty rectangles per commit; 4 → 16 costs about 1.1 KB of .bss. A compile definition, not a CMake option
ERUI_IMAGE_REGISTRY_MAX128Registered images, about 80 bytes each. Past the limit an image is refused and does not draw, so keep it at or above the asset count
ERUI_FONT_POOL_BYTES0A static pool for fonts loaded at runtime; 0 disables er_font_load
ERUI_SHADOWS, ERUI_3D_TRANSFORMS, ERUI_GRADIENT, ERUI_BILINEAR_SCALEvariesFeatures that cost code and scratch; off is free

The vector rasterize has its own pools, and unlike the pixel scratch they must live in internal RAM on a PSRAM board, because the scanline loops touch them per pixel:

FlagDefaultBounds
ERUI_MAX_VECTOR_NODES8<Svg> nodes with geometry at once
ERUI_VECTOR_PAINTS_MAX16Shapes per <Svg>
ERUI_VECTOR_MAX_POINTS, ERUI_VECTOR_MAX_EDGES2048Flattened points, and edges, in one shape
ERUI_VECTOR_MAX_ROW1024The widest vector node, in pixels
ERUI_ARC_MAX_RADIUS, ERUI_ARC_SPAN_CACHE255, 8The arc widget's shared span cache, about 4 KB at the defaults

Turn the perf overlay on and its VEC n/8 IMG n/128 line shows the pools filling; a screen missing an asset reads as a full image pool.

The framebuffer​

The biggest single number on most boards, and the backend's to choose:

  • Full framebuffer. The ESP32-S3 example keeps an 800×480 framebuffer in PSRAM; the RP2040 example keeps a 240×280 RGB565 framebuffer in SRAM, 131 KB of its 264 KB.
  • Banded. A backend with no room for a framebuffer keeps a band of band_height rows and lets the panel's memory retain the rest. The CYD example's two 40-row RGB565 strips are about 19 KB each, in place of a 150 KB framebuffer, and they ping-pong so drawing overlaps the SPI transfer. ER_LCD_BANDED_ROWS sets the height: smaller is less RAM and more transfers.

Flow A: the JavaScript heap​

Flow A adds QuickJS, React, the reconciler and your app's live objects. Four settings in ErRuntimeConfig control it:

  • memory_limit caps the heap so an app that leaks gets a JavaScript out-of-memory error rather than exhausting the system heap under the display driver.
  • malloc_functions decides where the heap lives. Left NULL, the bridge's own allocator is used and reports real block sizes. If you supply one to put the heap in PSRAM or SDRAM (the ESP32-S3 example does), its js_malloc_usable_size must return the true block size (heap_caps_get_allocated_size, tlsf_block_size, malloc_usable_size). A stub returning 0 silently disables garbage collection, and the heap falls a few KB per re-render forever; er_runtime_init warns when it detects this.
  • gc_threshold sets a floor under QuickJS's collection trigger. With a large external heap the default trigger recomputes to 1.5× the live set and mark-sweeps far too often; a floor cut 18% off a measured workload on the S3.
  • max_stack_size is the JavaScript stack-overflow guard; set it below the calling task's stack, as the next paragraph explains. Left 0, QuickJS keeps its own default.

The interpreter's stack is the C stack of the task that calls into it: every JavaScript frame lives there. Keep that task's stack in internal RAM and size it generously; the ESP32-S3 example uses a 64 KB main task stack and sets max_stack_size to three quarters of it. A JS "stack overflow" or an RTOS stack panic means raise it.

Sizes to plan around, measured on the ESP32-S3 with the thermostat: the vendor bytecode (React, the reconciler and the library) is about 140 KB in the container, and the QuickJS bridge's own tables are 22 KB, which sit in PSRAM at no measurable cost. Flow B has none of this.

How the examples are sized​

ESP32-S3ESP32 CYDRP2040
RAM512 KB internal + 8 MB PSRAM~208 KB free at boot, largest block ~110 KB; no PSRAM264 KB SRAM
FlowABB
Framebuffer800×480 in PSRAMTwo 40-row RGB565 bands, ~37 KB240×280 RGB565 in SRAM, 131 KB
JS heapIn PSRAMnonenone
Node pooldefaultloweredsized to the watch face
Flow B arena32 KB in its Flow B build2 KB4 KB
Animation poolsdefault16 animations, 8 values, 4 groups16 animations, 8 values, 4 groups
Image registrydefault168
Damage rectangles1644
Stack64 KB main task (36 KB with Wi-Fi)20 KB main task8 KB, both scratch banks, guarded
Shadows, gradientsshadows offshadows offshadows, gradients and keyboard off
Free after bootlogged at boot~165 KB internalfits with the framebuffer

The CYD logs free RAM at boot and again after start-up, the ESP32-S3 once after boot and in every alive: line; watch the later number as you add features. The three CMakeLists.txt files are the worked examples of the flags above.

Symptoms and causes​

SymptomLikely cause
An image draws as a hole; IMG reads full in the overlayThe image registry is full: raise ERUI_IMAGE_REGISTRY_MAX
A <Svg> stops drawing; VEC shows !FULLMore vector nodes than ERUI_MAX_VECTOR_NODES, or more shapes than ERUI_VECTOR_PAINTS_MAX in one
A fade or rotation is clipped, or a node will not fade at allThe node is larger than the scratch buffer: raise ERUI_SCRATCH_WIDTH and ERUI_SCRATCH_HEIGHT or ERUI_TRANSFORM_WIDTH and ERUI_TRANSFORM_HEIGHT, or shrink the node
JS "stack overflow", or an RTOS stack-overflow panicThe host task's stack is too small for the reconciler's recursion
The RP2040 reboots and logs rebooted by the watchdogA frame or a driver hung, or the stack overflowed into its guard; a debug stack= heartbeat near 8192 points at the stack
Free heap falls a few KB per re-render and never recovers (Flow A)The custom allocator's js_malloc_usable_size returns 0, so the collector never runs
A long list stops adding rowsThe node pool is full (ERUI_MAX_NODES), or in Flow B the list is past ER_AOT_LIST_CAP
A no-PSRAM ESP32 fails to allocate its band buffersInternal RAM is fragmented: shrink ER_LCD_BANDED_ROWS, or free DMA-capable RAM elsewhere