Drawing directly to the display, ignoring or subeverting object model - possible?

I have a project that uses ESP32S3 with SPI AMOLED display.

The screen is going to contain a number of usual widgets, text and graphical indicators, for which I would like to use lvgl, for painless rendering of the text and for antialiasing. These elements are rarely updated, and rendering is allowed to be slow.

Another, big, section of the screen will display oscilloscope-type trace of a real-time signal. The target is to draw the next, approximately 1.5K pixels, strip 25 times per second.

For this “fast” display, I do not want any blending, object overlap detection, or canvas buffers. Full image will only exist in the display controller RAM, and not take MCU resources to maintain.

After reading documentation, it seems that the usual advice to use canvas does not fit my use case, and I could not find anything else suitable.

My question:

What could be a reasonable way to achieve this, without totally breaking off with lvgl and resorting to display driver primitives?

Draw callback functions look promising. Would I be able to define a custom draw event callback on an object covering the “fast display area” maybe? Are such callbacks allowed to use “display flash” callback? How to control when exactly will it run?

Or maybe some other options exist?

Thank you!

I ended up with the code that, in the main loop, first calls lv_task_handler(), and after that, the driver function that updates the area (the same function that is registered with lv_display_set_flush_cb()).

Ugly, but I could not come up with anything better.

To make this report complete: I came up with an idea how it should be possible to solve my task with lvgl proper, without “punching holes in layers”. It almost works, but does not.

Because at every update a specific slice of the screen needs to be redrawn, I created an array of objects, one per slice, with a draw callback to redraw the curve in this slice. On each update, I passed new data to one slice object and invalidated it to trigger redraw.

It worked, rather slow, in a small test, but ran out of memory with 75 slices with 6 lines in each. So I had to fall back to the ugly way, with direct use of the display driver API.

As a side note, I was a little dismayed when I realized that lvgl uses malloc and does not check if allocation succeeded…

where is this done?

This is where I was getting memory access violation:

lv_draw_task_t * lv_draw_add_task(lv_layer_t * layer, const lv_area_t * coords)
{
    LV_PROFILER_BEGIN;
    lv_draw_task_t * new_task = lv_malloc_zeroed(sizeof(lv_draw_task_t));

    new_task->area = *coords;   // <<<<<<<<<<<<<<<< here
    new_task->_real_area = *coords;
    new_task->clip_area = layer->_clip_area;
    ...

src/draw/lv_draw.c:95
(I did not check that it was new_task fault and not coords, but it did work with smaller number of elements. I think that coords was not at fault. It was a simple local variable in the code.)

lv_malloc_zeroed I am sure has an assert wrapper in that function that tests if the allocation was successful. It is something that is able to be turned on and off. You would have to look in your lv_conf.h file to see if the allocation checks are turned on/off.

I don’t have any lv_conf.h lol.

I use lvgl as “managed component” in esp-idf, and don’t change any settings other that which fonts to include. It may be that they turn off checks by default (which is probably unwise)…

You do/ it is somewhere in the code you are using. If you didn’t it wouldn’t compile. You have to look around for it.

Yes, I know. I think that the .h file is generated from Kconfig my some CMakefine in the toolkit, but I did not bother to figure out how and when.

This is the code for the malloc function.

void * lv_malloc_zeroed(size_t size)
{
    LV_TRACE_MEM("allocating %lu bytes", (unsigned long)size);
    if(size == 0) {
        LV_TRACE_MEM("using zero_mem");
        return &zero_mem;
    }

    void * alloc = lv_malloc_core(size);
    if(alloc == NULL) {
        LV_LOG_INFO("couldn't allocate memory (%lu bytes)", (unsigned long)size);
#if LV_LOG_LEVEL <= LV_LOG_LEVEL_INFO
        lv_mem_monitor_t mon;
        lv_mem_monitor(&mon);
        LV_LOG_INFO("used: %zu (%3d %%), frag: %3d %%, biggest free: %zu",
                    mon.total_size - mon.free_size, mon.used_pct, mon.frag_pct,
                    mon.free_biggest_size);
#endif
        return NULL;
    }

    lv_memzero(alloc, size);

    LV_TRACE_MEM("allocated at %p", alloc);
    return alloc;
}

but you are correct there is no guard against a failed allocation in the draw task.

The lv_draw_add_task function should look like this…

lv_draw_task_t * lv_draw_add_task(lv_layer_t * layer, const lv_area_t * coords)
{
    LV_PROFILER_BEGIN;
    lv_draw_task_t * new_task = lv_malloc_zeroed(sizeof(lv_draw_task_t));
    LV_ASSERT_MALLOC(new_task);

    new_task->area = *coords;
    new_task->_real_area = *coords;
    new_task->clip_area = layer->_clip_area;
    ...

@kisvegabor

1 Like

At this point in the game you would need to enable logging in LVGL in order to see where the failure is actually occurring.