This project demonstrates high-performance SVG animations using LVGL v9, ThorVG, and LVGL_CPP on the Seeed Studio XIAO ESP32S3 paired with the Round Display (240x240).
Important
Start here: This repository is designed as a companion to the Animation workshop tutorial. The core value of this project is found within that guide, which walks you through five distinct phases of optimization.
- Hardware: Seeed Studio XIAO ESP32S3 + Seeed Round Display Shield.
- Environment: ESP-IDF v6.1.
- Build and flash:
idf.py build idf.py -p /dev/ttyACM0 flash monitor
It is important to distinguish between the generic C++ wrappers and the specific hardware drivers used in this workshop:
lvgl_cpp(library): A generic, platform-agnostic C++20 wrapper for LVGL. It runs on Linux, Windows, STM32, or ESP32. It provides the clean, object-oriented API (e.g.,lvgl::Display,lvgl::Image).main/sys/lvgl_port.cpp(project code): The specific "bridge" code that connects the genericlvgl_cpplibrary to the ESP32-S3 hardware (DMA, SPI, PSRAM). This workshop is designed as a guided journey from a "naive" implementation (7 FPS) to a "premium" high-performance animation (30 FPS). You can switch between these optimization levels to see and feel the impact of different hardware and software strategies.
You can choose your optimization level in two ways:
-
Menuconfig (recommended):
- Run
idf.py menuconfig. - Navigate to
Animation Workshop. - Find the Workshop Optimization Phase setting and enter a number from 1 to 5.
- Save and re-flash.
- Run
-
Header override:
- Open
main/workshop_config.h. - The file maps Kconfig macros to technical constants. You can manually force a phase here by redefining
WORKSHOP_PHASE.
- Open
| Phase | Bottleneck | Target FPS | Primary Learning |
|---|---|---|---|
| Phase 1 | SPI bus and CPU | ~9 FPS | Baseline implementation. CPU wait-loops on display I/O. |
| Phase 2 | Graphics Bus | ~15 FPS | Boosting SPI to 80MHz. Identifying the "Tiling Problem". |
| Phase 3 | Raster Pipeline | ~9 FPS (Regression!) | Double-buffering reduces tear but tiling overhead hurts FPS. |
| Phase 4 | Expert Tuning | ~25 FPS | Full-frame buffers in PSRAM and Xtensa SIMD Intrinsics. |
| Phase 5 | Native | ~30+ FPS | "Large Partial" Internal SRAM Buffering: Bypassing PSRAM latency with SIMD. |
- Issue: The display would flicker, and the touch controller would return random timeouts.
- Cause: GPIO 43 and 44 are the default pins for Hardware UART0. Logging bit-banged signals directly into the display controllers.
- Fix: Switched to Native USB Serial/JTAG Controller and disabled UART console.
- Issue: Complex SVG rendering would cause I2C timeouts.
- Cause: Vector rendering occupied 100% of the CPU, starving the I2C driver task.
- Fix: Boosted CPU to 240MHz and added a mandatory 5ms delay in the LVGL task loop.
- Issue: Larger display buffers (e.g., 80 lines) caused watchdog resets.
- Cause: Buffers were stealing the internal RAM required by ThorVG for vertex calculations.
- Fix: Settled on 20-line double buffers.
- Issue: Switching animals caused sporadic crashes.
- Cause: C++ lambda objects were being destroyed while LVGL still held pointers to them.
- Fix: Switched to raw C function pointers for animation callbacks.
- Issue: Despite enabling
CONFIG_LV_DRAW_SW_ASM_CUSTOM, performance fell from ~30 FPS to ~14 FPS in Phase 5. Visual artifacts (corruption) appeared when forcing assembly usage. - Root cause 1 (silent build failure): The
espressif__esp_lvgl_portcomponent had a hard version check (< 9.2.0) inCMakeLists.txt, causing it to silently skip compiling the S3-optimized assembly files for our LVGL 9.4 build. - Root cause 2 (API mismatch):
- Signatures: LVGL v9.4 hook macros (e.g.,
LV_DRAW_SW_RGB888_BLEND_NORMAL_TO_RGB888) require 3 arguments (dsc,dest_px,src_px), whereas the generic assembly routines only accepted 1. - Struct layout: The assembly code expects a specific legacy struct layout (starting with
opa). LVGL 9.4'slv_draw_sw_blend_fill_dsc_tstarts withdest_buf. Passing the pointer directly caused the assembly to interpret the destination pointer as opacity, leading to garbage reads. - Color format: The ESP32 assembly
fillroutines only support 32-bit colors (reading R, G, B bytes). Passing a pointer to a 16-bitlv_color_tcaused the assembly to read adjacent stack memory (garbage) as color channels.
- Signatures: LVGL v9.4 hook macros (e.g.,
- Fix: Implemented an External SIMD Patch Component (
components/lvgl_s3_simd_patch).- Architecture: We keep
espressif__esp_lvgl_portas a pristinemanaged_component. - Injection: The patch component locates the S3 assembly files within
managed_componentsand compiles them externally. - Shim: It builds a C shim (
shim.c) to bridge the struct and color format mismatches. - Linking: It uses header injection and
-ulinker flags to wire everything into the final binary.
- Architecture: We keep
- Result: Restored stable 30+ FPS while keeping dependencies clean.
- USB console: Always use USB Serial/JTAG on the S3 to avoid GPIO conflicts.
- Vector sizing: Render SVGs at a small base resolution (e.g., 180x180) and let LVGL scale them.
- Double buffering: Use internal DMA memory and the
__builtin_bswap16intrinsic.