I have tried different configuration on my Raspberry Pi 4 (4G). And I found that the Widgets demo is always the big problem.
The FPS drops to 20 (Benchmark Results shows the avg is 70 ), when scrolling the second tab (Analytics)in Widgets demo .
In my project ,the page is more complicated than the Widgets demo, so how to improve the fps ?
Here is a Performance Analysis from gpt:
LVGL Benchmark Results
Test Environment
- Hardware: Raspberry Pi 4 (4G)
- System: Linux (raspberrypi)
- Screen Resolution: 1280x800 @ 59Hz (HDMI-A-1)
- LVGL Version: 9.6.0 dev
- Build Configuration: [configs/get_started.defconfig](file:///lv_port_linux/configs/get_started.defconfig)
- Backend: DRM + EGL (GBM surface)
- Render Engine: NanoVG (GLES2 backend)
LV_DRAW_SW_DRAW_UNIT_CNT=2(but NanoVG backend forcestile_cnt=1, does not participate in rendering)LV_DEF_REFR_PERIOD=8ms- VSync: Disabled (
eglSwapInterval(0))
- Run Command:
sudo ./build/bin/lvglsim -D benchmark
Key Code Changes
To resolve the NanoVG backend black-screen issue, tile_cnt=1 is enforced at [lv_linux_drm_egl.c:131-135](file:///home/pi/Desktop/lvgl95/lv_port_linux/lvgl/src/drivers/display/drm/lv_linux_drm_egl.c#L131-L135):
#if LV_USE_DRAW_NANOVG
/* NanoVG renders the full frame directly to the EGL surface's default
* framebuffer. Tiling would create child layers with FBOs, causing
* NanoVG to render off-screen instead of to the visible surface. */
lv_display_set_tile_cnt(display, 1);
#endif
Benchmark Results
| Name | Avg. CPU | Avg. FPS | Avg. time | render time | flush time |
|---|---|---|---|---|---|
| Empty screen | 13% | 84 | 21 | 10 | 11 |
| Moving wallpaper | 15% | 63 | 14 | 0 | 14 |
| Single rectangle | 11% | 81 | 11 | 0 | 11 |
| Multiple rectangles | 11% | 82 | 11 | 0 | 11 |
| Multiple RGB images | 14% | 83 | 10 | 0 | 10 |
| Multiple ARGB images | 10% | 82 | 10 | 0 | 10 |
| Rotated ARGB images | 14% | 87 | 10 | 0 | 10 |
| Multiple labels | 26% | 87 | 9 | 5 | 4 |
| Screen sized text | 33% | 46 | 20 | 20 | 0 |
| Multiple arcs | 12% | 51 | 18 | 2 | 16 |
| Containers | 20% | 43 | 20 | 7 | 13 |
| Containers with overlay | 18% | 39 | 23 | 7 | 16 |
| Containers with opa | 18% | 43 | 21 | 8 | 13 |
| Containers with opa_layer | 18% | 13 | 70 | 43 | 27 |
| Containers with scrolling | 23% | 42 | 22 | 10 | 12 |
| Widgets demo | 14% | 73 | 15 | 5 | 10 |
| All scenes avg. | 16% | 62 | 18 | 7 | 11 |
Performance Analysis
Overall Performance
- Average 62 FPS, CPU 16% — Smooth overall, usable
- render 7ms / flush 11ms — Flush takes the majority; the bottleneck is GPU compositing + eglSwapBuffers
Excellent Performance (>80 FPS)
Empty screen / Single rectangle / Multiple rectangles / Multiple RGB/ARGB images / Rotated ARGB images / Multiple labels — Pure GPU drawing that NanoVG excels at, nearly hitting the refresh rate limit.
Performance Bottleneck Scenarios
| Scenario | FPS | Problem Analysis |
|---|---|---|
| Containers with opa_layer | 13 | Worst. opa_layer requires NanoVG to allocate FBO for off-screen rendering then composite; layer creation/readback overhead is huge (render 43ms + flush 27ms) |
| Screen sized text | 46 | Large text blocks. NanoVG text rendering tessellates per glyph, CPU 33% is high |
| Containers (overlay/opa/scrolling) | 39~43 | Containers + semi-transparent overlay trigger multiple layer compositions, flush 13~16ms |
| Multiple arcs | 51 | Arc path tessellation is heavy, flush 16ms |
| Moving wallpaper | 63 | Large image displacement, flush 14ms (render 0ms indicates pure blit) |
Optimization Suggestions (Sorted by Gain)
-
opa_layer scenario (largest bottleneck): Check the FBO reuse logic in [lv_draw_nanovg_layer.c](file:///home/pi/Desktop/lvgl95/lv_port_linux/lvgl/src/draw/nanovg/lv_draw_nanovg_layer.c) to confirm whether the FBO cache is hit and reused every frame; missing the cache is extremely costly.
-
High flush ratio: Enable
eglSwapInterval(1)to enable VSync (currently 0), reducing invalid refreshes to 60Hz, lowering CPU/GPU usage and avoiding tearing. ModifyeglSwapInterval(ctx->egl_display, 0)in [lv_opengles_egl.c](file:///home/pi/Desktop/lvgl95/lv_port_linux/lvgl/src/drivers/opengles/lv_opengles_egl.c). -
Text rendering: Pre-generate glyph atlas (NanoVG’s
nvgCreateFontMemalready supports this), confirm the font is not reloaded every frame. -
Arc optimization: Check whether the SW fallback for arcs is enabled; a pure GPU path should be faster.
Conclusion
Current performance is reasonable for Raspberry Pi + NanoVG. If you don’t need semi-transparent composition effects like opa_layer, the overall experience is already smooth. For further optimization, prioritize FBO cache hit rate and VSync configuration.
Config B: OpenGL ES Draw Unit + tile_cnt=2 (2026-07-31)
Test Environment
- Hardware/System/Screen Resolution: Same as above (Raspberry Pi, 1280x800 @ 59Hz)
- LVGL Version: 9.6.0 dev
- Render Engine: OpenGL ES Draw Unit (not NanoVG)
- Backend: DRM + EGL (GBM surface)
- Tiling: Enabled (
CONFIG_LV_DRAW_SW_DRAW_UNIT_CNT=2, tile_cnt=2) LV_DEF_REFR_PERIOD=8ms, VSync disabled
Key Code Changes
To support the OpenGL ES Draw Unit under tiling mode, in [lv_draw_opengles.c dispatch()](file:///home/pi/Desktop/lvgl95/lv_port_linux/lvgl/src/draw/opengles/lv_draw_opengles.c#L236-L265) the tile layer reuses the display layer_head’s texture (tile layer shares draw_buf):
if(layer->draw_buf == disp->layer_head->draw_buf) {
texture = (unsigned int)(uintptr_t)disp->layer_head->user_data;
}
else {
int32_t w = lv_area_get_width(&layer->buf_area);
int32_t h = lv_area_get_height(&layer->buf_area);
texture = create_texture(w, h, NULL);
}
The tile_cnt=1 restriction in [lv_linux_drm_egl.c](file:///home/pi/Desktop/lvgl95/lv_port_linux/lvgl/src/drivers/display/drm/lv_linux_drm_egl.c#L131) is now applied only to NanoVG; the OpenGL ES backend takes the native tiling path.
Benchmark Results
| Name | Avg. CPU | Avg. FPS | Avg. time | render time | flush time |
|---|---|---|---|---|---|
| Empty screen | 16% | 110 | 0 | 0 | 0 |
| Moving wallpaper | 30% | 98 | 9 | 6 | 3 |
| Single rectangle | 7% | 123 | 0 | 0 | 0 |
| Multiple rectangles | 15% | 123 | 0 | 0 | 0 |
| Multiple RGB images | 10% | 123 | 1 | 1 | 0 |
| Multiple ARGB images | 12% | 123 | 1 | 1 | 0 |
| Rotated ARGB images | 10% | 122 | 0 | 0 | 0 |
| Multiple labels | 21% | 122 | 3 | 3 | 0 |
| Screen sized text | 13% | 122 | 3 | 1 | 2 |
| Multiple arcs | 24% | 118 | 4 | 4 | 0 |
| Containers | 9% | 123 | 0 | 0 | 0 |
| Containers with overlay | 23% | 101 | 8 | 6 | 2 |
| Containers with opa | 10% | 123 | 0 | 0 | 0 |
| Containers with opa_layer | 10% | 123 | 1 | 1 | 0 |
| Containers with scrolling | 29% | 97 | 8 | 8 | 0 |
| Widgets demo | 22% | 57 | 28 | 27 | 1 |
| All scenes avg. | 16% | 113 | 3 | 3 | 0 |
Performance Analysis
- Average 113 FPS (CPU 16%) — +82% over NanoVG (62 FPS), excellent overall performance
- render 3ms / flush 0ms — Flush is nearly zero, the bottleneck is completely eliminated
Comparison with NanoVG Config A
| Scenario | NanoVG (FPS) | OpenGL ES tile=2 (FPS) | Gain |
|---|---|---|---|
| All scenes avg. | 62 | 113 | +82% |
| Containers with opa_layer | 13 | 123 | +846% (largest improvement) |
| Screen sized text | 46 | 122 | +165% |
| Containers with opa | 43 | 123 | +186% |
| Multiple arcs | 51 | 118 | +131% |
| Containers with scrolling | 42 | 97 | +131% |
| Empty screen | 84 | 110 | +31% |
| Widgets demo | 73 | 57 | -22% (only regression) |
Key Findings
- opa_layer 13 → 123 FPS: The OpenGL ES draw unit uses
glFramebufferTexture2Dto directly reuse the layer_head texture, bypassing NanoVG’s FBO create/readback path; the bottleneck is completely resolved - Text rendering greatly improved: 13% CPU (vs NanoVG 33%); the OpenGL ES draw unit uses
draw_to_texture+ SW subtask to efficiently cache glyphs - Widgets demo regresses 22%: 73 → 57 FPS, suspected that some complex components (tables/charts) in this demo don’t use the cache path under the OpenGL ES draw unit; render 27ms dominates, needs further investigation
- Flush time drops to zero: The OpenGL ES draw unit renders directly to the display texture; flush_cb only does
eglSwapBuffers, with almost no overhead
Config C: NanoVG (GLES2) + tile_cnt=2 (2026-07-31)
Configuration
- Backend: NanoVG GLES2 (CONFIG_LV_USE_DRAW_NANOVG=y, CONFIG_LV_NANOVG_BACKEND_GLES2=y)
- tile_cnt=2 (CONFIG_LV_DRAW_SW_DRAW_UNIT_CNT=2, the tile_cnt=1 restriction in lv_linux_drm_egl.c is disabled)
- Code change: In lv_draw_nanovg.c CHILD_CREATED, for tile layers (parent==NULL), skip FBO allocation, keep user_data=NULL, letting NanoVG render to the default framebuffer
- Build: Release (-O3 -DNDEBUG)
- Screen: 1280x800 (HDMI)
- Letter cache: 2048, FBO cache: 4
- LV_DEF_REFR_PERIOD: 8ms
Test Results
| Name | Avg. CPU | Avg. FPS | Avg. time | render time | flush time |
|---|---|---|---|---|---|
| Empty screen | 16% | 65 | 12 | 2 | 10 |
| Moving wallpaper | 16% | 48 | 19 | 3 | 16 |
| Single rectangle | 14% | 72 | 13 | 1 | 12 |
| Multiple rectangles | 13% | 56 | 15 | 1 | 14 |
| Multiple RGB images | 11% | 65 | 13 | 1 | 12 |
| Multiple ARGB images | 13% | 60 | 14 | 2 | 12 |
| Rotated ARGB images | 13% | 70 | 12 | 1 | 11 |
| Multiple labels | 20% | 68 | 12 | 6 | 6 |
| Screen sized text | 30% | 39 | 23 | 23 | 0 |
| Multiple arcs | 14% | 43 | 20 | 3 | 17 |
| Containers | 17% | 37 | 24 | 8 | 16 |
| Containers with overlay | 13% | 31 | 29 | 6 | 23 |
| Containers with opa | 18% | 39 | 24 | 8 | 16 |
| Containers with opa_layer | 20% | 21 | 43 | 25 | 18 |
| Containers with scrolling | 19% | 36 | 25 | 11 | 14 |
| Widgets demo | 13% | 57 | 26 | 9 | 17 |
| All scenes avg. | 16% | 50 | 19 | 6 | 13 |
Three-Config Comparison
| Metric | Config A: NanoVG tile=1 | Config B: OpenGL ES tile=2 | Config C: NanoVG tile=2 |
|---|---|---|---|
| All scenes avg. FPS | 62 | 113 | 50 |
| opa_layer FPS | 13 | 123 | 21 |
| Screen sized text | 46 | 122 | 39 |
| Widgets demo | 73 | 57 | 57 |
| flush time | 11ms | 0ms | 13ms |
| render time | 8ms | 3ms | 6ms |
Key Findings
- NanoVG + tile=2 actually degrades performance: Average 50 FPS (vs 62 FPS at tile=1), mainly due to flush time rising from 11ms to 13ms
- Flush time rises significantly: 13ms (vs 11ms at tile=1, 0ms at OpenGL ES tile=2); tile layers sharing the framebuffer causes extra sync/invalidation overhead
- opa_layer slightly improved but still poor: 21 FPS (vs 13 FPS at tile=1), because tiling offloads some non-opa_layer regions
- Widgets demo flat: 57 FPS, same as OpenGL ES tile=2, possibly limited by other factors
- Conclusion: The NanoVG architecture is not suitable for the tile-shared framebuffer mode; performance drops instead of improving. OpenGL ES tile=2 remains the optimal configuration
Config D: OpenGL ES Draw Unit + tile_cnt=4 (2026-07-31)
Configuration
- Backend: OpenGL ES Draw Unit (CONFIG_LV_USE_DRAW_OPENGLES=y)
- tile_cnt=4 (CONFIG_LV_DRAW_SW_DRAW_UNIT_CNT=4)
- Build: Release (-O3 -DNDEBUG)
- Screen: 1280x800 (HDMI)
- Other: Same as Config B
Test Results
| Name | Avg. CPU | Avg. FPS | Avg. time | render time | flush time |
|---|---|---|---|---|---|
| Empty screen | 27% | 110 | 1 | 1 | 0 |
| Moving wallpaper | 23% | 108 | 8 | 4 | 4 |
| Single rectangle | 7% | 123 | 0 | 0 | 0 |
| Multiple rectangles | 11% | 122 | 0 | 0 | 0 |
| Multiple RGB images | 13% | 123 | 1 | 1 | 0 |
| Multiple ARGB images | 13% | 123 | 1 | 1 | 0 |
| Rotated ARGB images | 10% | 123 | 0 | 0 | 0 |
| Multiple labels | 25% | 123 | 3 | 3 | 0 |
| Screen sized text | 11% | 119 | 3 | 1 | 2 |
| Multiple arcs | 25% | 116 | 5 | 5 | 0 |
| Containers | 9% | 123 | 0 | 0 | 0 |
| Containers with overlay | 25% | 96 | 9 | 8 | 1 |
| Containers with opa | 12% | 123 | 0 | 0 | 0 |
| Containers with opa_layer | 10% | 123 | 1 | 1 | 0 |
| Containers with scrolling | 39% | 87 | 10 | 10 | 0 |
| Widgets demo | 22% | 54 | 31 | 30 | 1 |
| All scenes avg. | 17% | 112 | 4 | 4 | 0 |
Config B vs D Comparison (OpenGL ES tile=2 vs tile=4)
| Metric | B: tile=2 | D: tile=4 | Difference |
|---|---|---|---|
| All scenes avg. FPS | 113 | 112 | -1 |
| All scenes avg. CPU | 16% | 17% | +1% |
| render time | 3ms | 4ms | +1ms |
| flush time | 0ms | 0ms | flat |
| Empty screen CPU | 16% | 27% | +11% (significantly higher) |
| Moving wallpaper CPU | 30% | 23% | -7% |
| Multiple labels CPU | 21% | 25% | +4% |
| Screen sized text | 122 FPS / 13% CPU | 119 FPS / 11% CPU | -3 FPS / -2% CPU |
| Containers with scrolling | 97 FPS / 29% CPU | 87 FPS / 39% CPU | -10 FPS / +10% CPU (clear regression) |
| Widgets demo | 57 FPS / 27ms render | 54 FPS / 30ms render | -3 FPS (slower render) |
Key Findings
- tile=4 yields no overall benefit: Average 112 FPS vs 113 FPS at tile=2, essentially flat
- CPU usage rises: 17% vs 16%, layer management overhead increases
- Some scenarios clearly regress:
- Empty screen CPU +11% (tile scheduling overhead dominates in empty scenes)
- Containers with scrolling -10 FPS / +10% CPU (frequent tile switching in scrolling scenes)
- Widgets demo -3 FPS (render 30ms vs 27ms, slower)
- Few scenarios slightly improve: Moving wallpaper CPU -7%, but only +10 FPS
- Confirms previous analysis: The OpenGL ES draw unit is a single-instance serial GPU pipeline; more tiles cannot parallelize, and instead add layer scheduling overhead
- Conclusion: tile=2 is the optimal value for the OpenGL ES draw unit; increasing further is not recommended
Config E: OpenGL ES Draw Unit + tile=2 + REFR_PERIOD=5ms (2026-07-31)
Configuration
- Backend: OpenGL ES Draw Unit (CONFIG_LV_USE_DRAW_OPENGLES=y)
- tile_cnt=2 (CONFIG_LV_DRAW_SW_DRAW_UNIT_CNT=2)
- Refresh period: 5ms (CONFIG_LV_DEF_REFR_PERIOD=5, theoretical limit 200 FPS)
- Build: Release (-O3 -DNDEBUG)
- Screen: 1280x800 (HDMI)
- Other: Same as Config B
Test Results
| Scenario | Avg. CPU | Avg. FPS | Avg. time | render time | flush time |
|---|---|---|---|---|---|
| Empty screen | 22% | 151 | 4 | 1 | 3 |
| Moving wallpaper | 27% | 99 | 8 | 6 | 2 |
| Single rectangle | 12% | 190 | 4 | 0 | 4 |
| Multiple rectangles | 15% | 168 | 4 | 0 | 4 |
| Multiple RGB images | 14% | 183 | 3 | 0 | 3 |
| Multiple ARGB images | 22% | 179 | 3 | 0 | 3 |
| Rotated ARGB images | 12% | 194 | 1 | 0 | 1 |
| Multiple labels | 26% | 151 | 4 | 3 | 1 |
| Screen sized text | 15% | 130 | 6 | 1 | 5 |
| Multiple arcs | 27% | 159 | 4 | 3 | 1 |
| Containers | 12% | 194 | 0 | 0 | 0 |
| Containers with overlay | 24% | 101 | 7 | 5 | 2 |
| Containers with opa | 12% | 196 | 0 | 0 | 0 |
| Containers with opa_layer | 13% | 181 | 3 | 1 | 2 |
| Containers with scrolling | 29% | 100 | 8 | 8 | 0 |
| Widgets demo | 24% | 79 | 28 | 27 | 1 |
| All scenes avg. | 19% | 153 | 5 | 3 | 2 |
Config B vs E Comparison (REFR_PERIOD 8ms vs 5ms)
| Metric | B: 8ms | E: 5ms | Difference |
|---|---|---|---|
| All scenes avg. FPS | 113 | 153 | +40 (+35%) |
| All scenes avg. CPU | 16% | 19% | +3% |
| Empty screen | 110 FPS | 151 FPS | +41 |
| Single rectangle | 123 FPS | 190 FPS | +67 |
| Multiple rectangles | 123 FPS | 168 FPS | +45 |
| Rotated ARGB images | 122 FPS | 194 FPS | +72 |
| Containers | 123 FPS | 194 FPS | +71 |
| Containers with opa | 123 FPS | 196 FPS | +73 |
| Containers with opa_layer | 123 FPS | 181 FPS | +58 |
| Multiple arcs | 118 FPS | 159 FPS | +41 |
| Multiple labels | 122 FPS | 151 FPS | +29 |
| Screen sized text | 122 FPS | 130 FPS | +8 |
| Moving wallpaper | 98 FPS | 99 FPS | +1 (GPU bottleneck) |
| Containers with overlay | 101 FPS | 101 FPS | 0 (GPU bottleneck) |
| Containers with scrolling | 97 FPS | 100 FPS | +3 |
| Widgets demo | 57 FPS | 79 FPS | +22 |
| flush time (avg) | 0ms | 2ms | +2ms |
Key Findings
- Average FPS significantly improved: 113 → 153 (+35%); REFR_PERIOD=5ms removed the 8ms-period FPS ceiling
- Light-load scenarios improve significantly: Simple scenes approach the theoretical 200 FPS limit (Single rectangle 190, Containers 194)
- Heavy-load scenarios are GPU-limited:
- Moving wallpaper 99 FPS (render 6ms, GPU saturated)
- Containers with overlay 101 FPS (render 5ms + flush 2ms)
- These two scenarios are bottlenecked by render time; shortening REFR_PERIOD does not help
- Widgets demo unexpectedly improves: 57 → 79 FPS (+22), possibly because more frequent refresh scheduling reduces inter-frame idle time
- CPU usage rises: 16% → 19% (+3%), more frequent refresh brings additional scheduling overhead
- Flush time increases: 0ms → 2ms, more frequent EGL swaps introduce some overhead
- Conclusion: REFR_PERIOD=5ms is an effective optimization direction, improving overall FPS by 35% at the cost of +3% CPU