I’m working with LVGL 9.5 on an ESP32 (ESP-IDF), and I have strong RAM constraints, so I cannot use lv_canvas or any large intermediate buffers.
My goal is to render filled arbitrary polygons dynamically (real-time), without allocating additional frame buffers or off-screen memory.
Current approach
- Using the SW renderer (no GPU)
- Drawing primitives manually inside a custom draw routine / task
- Tried:
lv_draw_line → only outlines
- Splitting shapes into multiple triangles → works partially, but produces visible seams/artifacts between triangles (color blending issues)
Constraints
- No canvas (RAM limitation)
- No full-screen buffering
- Real-time rendering (objects change every frame)
- Prefer to stay in SW renderer (ThorVG/vector backend seems too heavy and unclear on ESP32)
Questions
- Is there any native way in LVGL 9.5 to render filled polygons (convex or concave) without using canvas or vector graphics?
- Is there an internal API (e.g. low-level draw context) that supports triangle filling without artifacts between adjacent triangles?
- Are the seams between triangles expected due to rasterization/antialiasing? Is there a recommended way to avoid them?
- Is using ThorVG the only correct approach for proper polygon filling in LVGL 9.x?
- Any recommended pattern for real-time geometry rendering under tight RAM constraints?
You can see the lines inside the square
static void my_ui(void)
{
lv_obj_t * screen = lv_screen_active();
lv_obj_set_style_bg_color(screen, lv_color_white(), LV_PART_MAIN);
lv_obj_add_event_cb(screen, draw_cb_event, LV_EVENT_DRAW_TASK_ADDED, screen);
lv_obj_set_flag(screen, LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS, true);
}
static void draw_cb_event(lv_event_t * e)
{
lv_obj_t * obj = (lv_obj_t *)lv_event_get_target(e);
lv_draw_task_t * draw_task = lv_event_get_draw_task(e);
lv_draw_dsc_base_t * base_dsc = (lv_draw_dsc_base_t *)lv_draw_task_get_draw_dsc(draw_task);
if (base_dsc->part == LV_PART_MAIN){
// Get Parent Coordinates
lv_area_t obj_coords;
lv_obj_get_coords(obj, &obj_coords);
// Get Parent Center Point
int32_t cx = (obj_coords.x1 + obj_coords.x2) / 2;
int32_t cy = (obj_coords.y1 + obj_coords.y2) / 2;
// LV_LOG_USER("cx:%d - cy:%d", cx, cy);
// Create triangle with Primitives
lv_draw_triangle_dsc_t tri_dsc;
// Init with default config
lv_draw_triangle_dsc_init(&tri_dsc);
// Configurate triangle
tri_dsc.color = lv_palette_main(LV_PALETTE_GREEN);
// Triangle Size
int32_t b = 200;
// Set triangle points (x, y), remember images coordinates
// (0,0) are start points in top left
tri_dsc.p[0] = (lv_point_precise_t){cx - b/2, cy};
tri_dsc.p[1] = (lv_point_precise_t){cx , cy - b/2};
tri_dsc.p[2] = (lv_point_precise_t){cx + b/2, cy};
// Draw Triangle 1
lv_draw_triangle(base_dsc->layer, &tri_dsc);
// Set triangle points to make a square base in 2 triangles
tri_dsc.p[0] = (lv_point_precise_t){cx - b/2, cy};
tri_dsc.p[1] = (lv_point_precise_t){cx , cy + b/2};
tri_dsc.p[2] = (lv_point_precise_t){cx + b/2, cy};
// Draw Triangle 2
lv_draw_triangle(base_dsc->layer, &tri_dsc);
}
}
I ended up using two separate triangles and a thin line between them as a workaround. It was the only way I could avoid the visible seam/artifact that appears when rendering them together as a single shape.

float rotation = 0;
static void timer_cb(lv_timer_t * timer)
{
lv_obj_t * obj = (lv_obj_t *)lv_timer_get_user_data(timer);
rotation += 0.5f;
if(rotation >= 360.0f) rotation = 0.0f;
lv_obj_invalidate(obj);
}
static void my_ui(void)
{
lv_obj_t * screen = lv_screen_active();
lv_obj_set_style_bg_color(screen, lv_color_black(), LV_PART_MAIN);
lv_obj_add_event_cb(screen, draw_cb_event, LV_EVENT_DRAW_TASK_ADDED, screen);
lv_obj_set_flag(screen, LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS, true);
lv_timer_create(timer_cb, 33, screen);
}
static void square_draw(lv_obj_t * obj, lv_draw_task_t * draw_task,lv_draw_dsc_base_t * base, int32_t size, float rotate)
{
float angle = rotate * (3.1416f / 180.0f);
float c = cosf(angle);
float s = sinf(angle);
lv_area_t obj_coords;
lv_obj_get_coords(obj, &obj_coords);
int cx = (obj_coords.x1 + obj_coords.x2) / 2;
int cy = (obj_coords.y1 + obj_coords.y2) / 2;
int32_t h = size / 2;
lv_point_precise_t corners[4] = {
{cx - h, cy - h}, // top-left
{cx + h, cy - h}, // top-right
{cx + h, cy + h}, // bottom-right
{cx - h, cy + h}, // bottom-left
};
// Rotate around center point
for(int i = 0; i < 4; i++) {
float x = corners[i].x - cx;
float y = corners[i].y - cy;
corners[i].x = cx + (x * c - y * s);
corners[i].y = cy + (x * s + y * c);
}
// Draw triangle
lv_draw_triangle_dsc_t tri_dsc;
lv_draw_triangle_dsc_init(&tri_dsc);
tri_dsc.opa = LV_OPA_100;
int32_t offset = 0;
// Triángulo 1: top-left, top-right, bottom-right
tri_dsc.color = lv_palette_main(LV_PALETTE_RED);
tri_dsc.p[0] = (lv_point_precise_t){corners[0].x - offset, corners[0].y};
tri_dsc.p[1] = corners[1]; // don't touch
tri_dsc.p[2] = (lv_point_precise_t){corners[2].x - offset, corners[2].y};
lv_draw_triangle(base->layer, &tri_dsc);
// Triangle 2: top-left, bottom-right, bottom-left
tri_dsc.color = lv_palette_main(LV_PALETTE_RED);
tri_dsc.p[0] = (lv_point_precise_t){corners[0].x + offset, corners[0].y};
tri_dsc.p[1] = corners[2]; // don't touch
tri_dsc.p[2] = (lv_point_precise_t){corners[3].x + offset, corners[3].y};
lv_draw_triangle(base->layer, &tri_dsc);
// DRAW LINE
lv_draw_line_dsc_t line_dsc;
lv_draw_line_dsc_init(&line_dsc);
line_dsc.color = lv_palette_main(LV_PALETTE_GREY);
line_dsc.width = 2;
line_dsc.opa = LV_OPA_100;
line_dsc.p1 = corners[0];
line_dsc.p2 = corners[2];
lv_draw_line(base->layer, &line_dsc);
}