LVGL源码学习之渲染、更新过程(1)---标记和激活

LVGL版本:8.1

在正式探究源码前,先对几个和更新相关的名词进行说明,有些名词后面可能会混用:

  • 渲染(render):指的是将发生修改的对象,根据修改的内容,重新写入显示缓存;
  • 绘制(draw):在LVGL中可以等同于渲染概念;
  • 融合(blend):在LVGL中特指写入缓存的过程;
  • 刷写(flush):将显示缓存写入显示器,这是写入硬件的过程;
  • 更新/刷新(refresh):指的是渲染/绘制和更新两个过程的结合,即:将修改的对象显示到显示器屏幕上。

LVGL的层级概念:

显示器(Display)是绘制和显示像素(flush)的硬件对象;

屏幕(screen)是显示器下的高级根对象,一个显示器可以存在多个屏幕(同一时刻仅显示一个屏幕),但一个屏幕只对应一个显示器;

控件(Widgets),是LVGL图形界面的核心元素,是显示和交互的基石,上面所说的屏幕本质上也是一种控件。

缓冲模式(buffer):

       这里说的缓冲指的就是显示缓存,通常将单个缓冲大小设置为显示器的像素总数,如果内存紧缺,也可以设置一个较小的缓冲,同样可以实现正常的显示效果,只是效率会相对低一些。在LVGL中,可以设置单缓冲和双缓冲两种缓冲模式。

  • 单缓冲:缓冲同时用于刷写和更新,若当前正在刷写,则所有的渲染都需要等待,直至刷写完成;
  • 双缓冲:一个用于缓存刷写,另一个用于更新渲染,渲染完成后复制到另一个缓冲进行刷写(或交换地址进行刷写),两个缓冲并行工作;

更新模式:

  • 直写模式(direct):根据绝对位置坐标对显示缓存进行绘制,该模式下需要显示缓存大小等于显示器的像素总数,因此每片区域更新完毕后,可以直接将缓冲刷写到显示器上。如果设置了双缓冲,则每次渲染完毕将更新的区域记录下来,留给另一块缓冲渲染使用;
  • 全刷新(full_refresh):每次发生更新,总是重新绘制整个屏幕(而非区域重绘),该模式和缓冲数量无关:在单缓冲下刷写和渲染不能同时进行;如果使用双缓冲,可以在刷写时仅交换缓冲地址;
  • 普通模式(normal):又称分块模式(partial),上述两种模式标志位都为0时触发,分块刷新区域。和直接模式不同的是,普通模式可以设置较小的缓冲,分块刷写到显示器硬件。

       以上相关概念的介绍,将会在接下来的源码分析经常使用到。

更新过程

       更新源自于对象的修改,从显示器、屏幕到组件层对象的修改都可以触发更新,因此从标记这些修改开始,开启LVGL渲染更新的过程。

标记和激活

       每次修改样式后(包括手动修改、动画修改等),都会触发更新,告诉系统要对被修改的对象节点进行标记,具体包括标记无效区域(invalidate area)标记脏对象(dirty)两种。

//lv_obj_style.c
void lv_obj_refresh_style(lv_obj_t * obj, lv_style_selector_t selector, lv_style_prop_t prop)
{
    LV_ASSERT_OBJ(obj, MY_CLASS);

    if(!style_refr) return;

    lv_obj_invalidate(obj);   //标记无效区域

    lv_part_t part = lv_obj_style_get_selector_part(selector);

    if(prop & LV_STYLE_PROP_LAYOUT_REFR) {  //需要更新布局
        if(part == LV_PART_ANY ||
           part == LV_PART_MAIN ||
           lv_obj_get_style_height(obj, 0) == LV_SIZE_CONTENT ||
           lv_obj_get_style_width(obj, 0) == LV_SIZE_CONTENT) {
            lv_event_send(obj, LV_EVENT_STYLE_CHANGED, NULL);
            lv_obj_mark_layout_as_dirty(obj);   //标记脏数据
        }
    }
    if((part == LV_PART_ANY || part == LV_PART_MAIN) && (prop == LV_STYLE_PROP_ANY ||
       (prop & LV_STYLE_PROP_PARENT_LAYOUT_REFR))) {  //需要更新父节点的布局
        lv_obj_t * parent = lv_obj_get_parent(obj);
        if(parent) lv_obj_mark_layout_as_dirty(parent);  //标记脏数据
    }

    if(prop == LV_STYLE_PROP_ANY || (prop & LV_STYLE_PROP_EXT_DRAW)) {
        lv_obj_refresh_ext_draw_size(obj);  //更新额外绘制区域
    }
    lv_obj_invalidate(obj);  //标记无效区域

    if(prop == LV_STYLE_PROP_ANY ||
       ((prop & LV_STYLE_PROP_INHERIT) && ((prop & LV_STYLE_PROP_EXT_DRAW) || (prop & LV_STYLE_PROP_LAYOUT_REFR)))) {
        if(part != LV_PART_SCROLLBAR) {
            refresh_children_style(obj);
        }
    }
}

标记无效区域(invalidate area):

       所有无效区域都被存储到inv_areas[]中,且用inv_p标识当前的无效区域数量。

//lv_hal_disp.h
typedef struct _lv_disp_t {
    /*......*/
    /** Invalidated (marked to redraw) areas*/
    lv_area_t inv_areas[LV_INV_BUF_SIZE];  //数组,存储前面标记的所有无效区域坐标
    uint16_t inv_p;  //无效区域数量
    /*......*/
} lv_disp_t;

       具体标记函数为lv_obj_invalidate()

//lv_obj_pos.c
void lv_obj_invalidate(const lv_obj_t * obj)
{
    LV_ASSERT_OBJ(obj, MY_CLASS);

    /* 获取对象所在的区域坐标 */
    lv_area_t obj_coords;
    lv_coord_t ext_size = _lv_obj_get_ext_draw_size(obj);  //获取额外绘制大小
    lv_area_copy(&obj_coords, &obj->coords);
    obj_coords.x1 -= ext_size;
    obj_coords.y1 -= ext_size;
    obj_coords.x2 += ext_size;
    obj_coords.y2 += ext_size;

    lv_obj_invalidate_area(obj, &obj_coords);  //标记对象所在的无效区域
}

       底层调用_lv_inv_area(),对于全刷新模式(full_refresh),一旦对象发生样式修改,就会将全屏区域纳入无效区域,且仅更新一次。而其它模式下,将在更新任务到来之前,收集所有在此期间发生修改的无效区域,并记录数量。

//lv_refr.c
/**
 * 函数功能:将显示器某区域标记为无效区域
 * @param disp 指针,被标记区域属于哪个“显示器(displayer)”,就是“屏幕(screen)”的上层
 * @param area_p 指针,被标记的区域坐标范围(x1,y1,x2,y2)
 */
void _lv_inv_area(lv_disp_t * disp, const lv_area_t * area_p)
{
    if(!disp) disp = lv_disp_get_default();
    if(!disp) return;

    /* 传入区域为NULL时,将无效区域数组长度设为0,表示清除无效区域(invalidate area),这是个用于清除的特殊操作 */
    if(area_p == NULL) {
        disp->inv_p = 0;
        return;
    }

    lv_area_t scr_area;   //获取显示器的显示范围
    scr_area.x1 = 0;
    scr_area.y1 = 0;
    scr_area.x2 = lv_disp_get_hor_res(disp) - 1;
    scr_area.y2 = lv_disp_get_ver_res(disp) - 1;

    lv_area_t com_area;
    bool suc;

    suc = _lv_area_intersect(&com_area, area_p, &scr_area);  //取显示范围和无效区域的并集
    if(suc == false)  return; /* 超出显示范围,直接返回 */

    /* 在全刷新(full_refresh)模式下,只要发生样式更新,就设置无效区域为全屏,且仅更新一次 */
    if(disp->driver->full_refresh) {
        disp->inv_areas[0] = scr_area;  //刷新全屏,只需要设置第一个索引区域为全屏
        disp->inv_p = 1;   //只更新一次(全屏)
        lv_timer_resume(disp->refr_timer);   //恢复更新任务定时器,等待更新执行
        return;
    }
    /* 其它模式下,需要一个个将无效区域加入到数组中,并更新索引 */
    //有些设备只能匹配特定宽高的像素,需要对区域做舍入处理
    if(disp->driver->rounder_cb) disp->driver->rounder_cb(disp->driver, &com_area);

    /* 若当前无效区域是数组中已存在的区域的子集,则作废当前区域并返回 */
    uint16_t i;
    for(i = 0; i < disp->inv_p; i++) {
        if(_lv_area_is_in(&com_area, &disp->inv_areas[i], 0) != false) return;
    }

    /* 保存当前无效区域 */
    if(disp->inv_p < LV_INV_BUF_SIZE) {
        lv_area_copy(&disp->inv_areas[disp->inv_p], &com_area);
    }
    else {   /* 如果无效区域数量超出设定,则清空并从0保存 */
        disp->inv_p = 0;
        lv_area_copy(&disp->inv_areas[disp->inv_p], &scr_area);
    }
    disp->inv_p++;  //无效区域数量加1
    lv_timer_resume(disp->refr_timer);  //恢复更新任务定时器,告诉系统可以继续定时更新了
}

标记脏数据(dirty):

       当对象被修改时,有两种情况下可能会被标记为脏数据:

  • 如果带有更新布局的标志位(通常对象大小、位置、变换等样式会附带),就会被标记为脏数据以便后期更新布局;

       后面在布局更新时会再次提及,其最终目的还是为了标记无效区域

//lv_obj_pos.c
void lv_obj_mark_layout_as_dirty(lv_obj_t * obj)
{
    obj->layout_inv = 1;  //将对象标记为脏(无效invalidate)

    lv_obj_t * scr = lv_obj_get_screen(obj);
    scr->scr_layout_inv = 1;  //将对象所处的屏幕标记为脏(无效invalidate)

    lv_disp_t * disp = lv_obj_get_disp(scr);  //获取该对象所在的屏幕
    lv_timer_resume(disp->refr_timer);  //恢复显示器的刷新定时器,告诉系统该显示器可以继续定时更新了
}

        无论是无效区域还是脏数据的标记,在每次标记完后,都会启动更新任务的定时器(重复的启动仅以第一次有效),可以称这个过程为“激活”,在定时器到期后,系统会调用更新任务进行屏幕内容的刷新。

定时更新

       在注册显示器时,LVGL就为其创建了更新定时器,默认周期为30ms。上一节说到,在标记修改后会激活更新任务的定时器,就是启动该定时器,即:

//lv_hal_disp.c
lv_disp_t * lv_disp_drv_register(lv_disp_drv_t * driver)
{
    /*......*/
    disp->refr_timer = lv_timer_create(_lv_disp_refr_timer, LV_DISP_DEF_REFR_PERIOD, disp);
    /*......*/
}

       其中定时更新的函数_lv_disp_refr_timer()是本文的核心,内容有很多,主要包括布局更新区域合并区域分块刷新硬件刷写以及监视器更新(需要在配置文件激活),其中监视器内容本质上是重复上面的更新内容。下面将着重探究前面的几个重点部分。

//lv_refr.c
void _lv_disp_refr_timer(lv_timer_t * tmr)
{
    REFR_TRACE("begin");

    uint32_t start = lv_tick_get();
    volatile uint32_t elaps = 0;

    disp_refr = tmr->user_data;  //这里的user_data就是注册显示器时传入的display

#if LV_USE_PERF_MONITOR == 0 && LV_USE_MEM_MONITOR == 0
    /**
     * 如果使用了任意监视器,会在屏幕左下角(或右下角)实时显示CPU和内存使用情况,此时就不可以暂停更新定时器。
     * 否则必须暂停定时器,等待有脏数据产生再启动
     */
    lv_timer_pause(tmr);
#endif

    /* 更新各个屏幕内的布局(如有需要) */
    lv_obj_update_layout(disp_refr->act_scr);
    if(disp_refr->prev_scr) lv_obj_update_layout(disp_refr->prev_scr);

    lv_obj_update_layout(disp_refr->top_layer);
    lv_obj_update_layout(disp_refr->sys_layer);  //这个顺序更新,表明sys_layer > top_layer > act_scr

    /* 没有活跃的屏幕,则直接返回 */
    if(disp_refr->act_scr == NULL) {
        disp_refr->inv_p = 0;
        LV_LOG_WARN("there is no active screen");
        REFR_TRACE("finished");
        return;
    }

    lv_refr_join_area();  //合并无效区域,减少重叠区域带来的计算开销

    lv_refr_areas();  //刷新无效区域的内容到显示缓存,直写模式将直接刷写到显示器

    /* 如果存在无效区域更新 */
    if(disp_refr->inv_p != 0) {
        if(disp_refr->driver->full_refresh) {  //全刷新模式下,将在此刷写到显示器上
            draw_buf_flush();
        }

        /* 更新完毕后,清除所有无效区域 */
        lv_memset_00(disp_refr->inv_areas, sizeof(disp_refr->inv_areas));
        lv_memset_00(disp_refr->inv_area_joined, sizeof(disp_refr->inv_area_joined));
        disp_refr->inv_p = 0;

        elaps = lv_tick_elaps(start);
        /* 如果定义了监视器,则执行监视器回调函数 */
        if(disp_refr->driver->monitor_cb) {
            disp_refr->driver->monitor_cb(disp_refr->driver, elaps, px_num);
        }
    }

    lv_mem_buf_free_all();
    _lv_font_clean_up_fmt_txt();

    /*...省略监视器内容...*/

    REFR_TRACE("finished");
}

布局更新

       对显示器下的几个屏幕都进行一次布局上的更新,更新与否需要依据前面推送的脏数据,一旦某个屏幕上存在脏数据,就会对整个屏幕的内容进行布局更新。

//lv_obj_pos.c
void lv_obj_update_layout(const lv_obj_t * obj)
{
    static bool mutex = false;  //互斥信号量,最原始的实现方式
    if(mutex) {
        LV_LOG_TRACE("Already running, returning");
        return;
    }
    mutex = true;

    lv_obj_t * scr = lv_obj_get_screen(obj);  //获取当前对象所处的屏幕

    /* 更新布局,直到没有脏数据了,注意在更新过程中有可能产生新的脏数据 */
    while(scr->scr_layout_inv) {
        LV_LOG_INFO("Layout update begin");
        scr->scr_layout_inv = 0;  //将屏幕更新状态重置
        layout_update_core(scr);
        LV_LOG_TRACE("Layout update end");
    }

    mutex = false;
}
底层是一个递归调用更新:

做了两件事:

①常规布局更新。重新计算对象的大小和位置坐标(检查样式的变化),计算完毕后将对象坐标区域纳入无效区域;

②特殊布局更新。主要有两种:flex和grid布局,运用了这两种布局的对象会在这里进行额外更新。(如果用户有自己注册的布局,也会在这里更新)。

//lv_obj_pos.c
static void layout_update_core(lv_obj_t * obj)
{
    uint32_t i;
    uint32_t child_cnt = lv_obj_get_child_cnt(obj);  //获取孩子节点数量
    for(i = 0; i < child_cnt; i++) {  //遍历孩子节点,对这些节点做递归嵌套调用
        lv_obj_t * child = obj->spec_attr->children[i];
        layout_update_core(child);   //深度优先遍历
    }

    if(obj->layout_inv == 0) return;   //如果不是脏数据,则直接返回

    obj->layout_inv = 0;   //重置脏数据状态

    lv_obj_refr_size(obj);  //重新计算对象大小
    lv_obj_refr_pos(obj);  //重新计算目标位置坐标

    if(child_cnt > 0) {   //如果该对象有孩子节点,且使用了特殊布局,则运用该布局专属的回调函数进行额外更新
        uint32_t layout_id = lv_obj_get_style_layout(obj, LV_PART_MAIN);
        if(layout_id > 0 && layout_id <= layout_cnt) {
            void  * user_data = LV_GC_ROOT(_lv_layout_list)[layout_id - 1].user_data;
            LV_GC_ROOT(_lv_layout_list)[layout_id - 1].cb(obj, user_data);
        }
    }
}

       经过布局更新后,所有的脏数据都转变为了无效区域,接下来就是对所有的无效区域进行处理了。这部分内容留到下一篇继续分析。

<think>嗯,用户现在问的是另一段LVGL代码,看起来是关于图层处理任务调度的。之前用户已经询问过绘图单元的遍历任务分派,现在这段代码似乎是在处理更具体的情况,特别是当父图层存在且所有任务都已添加但当前图层的绘制任务头为空的时候。 首先,我需要理解这段代码的结构。代码分为两个主要部分:一个if条件判断一个else分支。在if条件满足的情况下,会遍历父图层的绘制任务,寻找特定类型的任务并更新其状态,然后触发分派请求。而在else的情况下,执行之前提到的绘图单元分派任务的过程。 用户可能想知道这段代码的作用,特别是if部分的逻辑,以及整个代码的执行流程。我需要解释清楚每个条件分支的目的,以及它们在LVGL绘图流程中的位置作用。用户可能是在调试或优化LVGL的绘图性能,或者在学习LVGL的内部机制,需要深入理解任务调度图层的管理。 接下来要注意代码中的关键点:比如检查layer->parent是否存在,all_tasks_added是否为真,draw_task_head是否为空。这些条件组合起来可能表示当前图层已经完成了任务的添加,但还没有任务需要处理,这时候需要从父图层中找到相关的图层绘制任务,并将其状态更新,以便后续处理。 在解释过程中,需要明确各个结构体的作用,比如lv_draw_task_t、lv_draw_image_dsc_t,以及状态转换(从WAITING到QUEUED)。同时,要说明lv_draw_dispatch_request()的作用,即请求重新分派任务,确保任务被及时处理。 在else分支中,用户可能已经熟悉之前的绘图单元遍历,但需要确认这段代码的位置,即当不满足if条件时,继续正常分派任务给各个绘图单元。这里可能需要强调条件分支的不同情况,以及它们如何影响整个渲染流程。 另外,用户可能会想知道这段代码LVGL源码中的位置,比如是否在lv_refr_area_part函数中,或者其它相关模块。需要指出代码所处的函数文件,帮助用户定位上下文。 最后,用户可能关心这段代码的执行结果,比如返回值task_dispatched的意义,以及它如何影响后续的渲染步骤,比如图层混合或显示刷新。需要将整个流程串联起来,让用户理解每个步骤的衔接作用。 需要确保解释清晰,结构分明,用例子或场景说明代码的应用情况。比如,当某个子图层的任务完成后,父图层如何处理其任务,或者当所有任务都添加完毕后,系统如何确保任务被正确执行。这些实际场景能帮助用户更好地理解代码的逻辑目的。 同时,注意用户的技术水平,可能对LVGL内部结构有一定了解,但需要更深入的细节。因此,解释时应兼顾深度易懂性,避免过于晦涩的技术术语,必要时举例说明结构体回调函数的作用。 总结来说,我需要分步骤解析代码的每个部分,说明条件分支的逻辑、结构体的作用、状态转换的意义,以及整个代码LVGL绘图流程中的位置影响。确保用户能够理解这段代码如何管理绘图任务的分派处理,尤其是在处理图层依赖任务状态时的机制。</think>这段代码LVGL图形库中**图层(Layer)绘制任务调度**的关键逻辑,主要用于处理以下两种情况: --- ### 代码逻辑解析 #### 一、**条件分支1:处理父图层的关联绘制任务** ```c if(layer->parent && layer->all_tasks_added && layer->draw_task_head == NULL) { // 在父图层中查找需要处理的关联任务 lv_draw_task_t * t_src = layer->parent->draw_task_head; while(t_src) { if(t_src->type == LV_DRAW_TASK_TYPE_LAYER && t_src->state == LV_DRAW_TASK_STATE_WAITING) { lv_draw_image_dsc_t * draw_dsc = t_src->draw_dsc; if(draw_dsc->src == layer) { t_src->state = LV_DRAW_TASK_STATE_QUEUED; // 更新任务状态 lv_draw_dispatch_request(); // 请求重新分派任务 break; } } t_src = t_src->next; } } ``` **触发条件**(三者需同时满足): 1. `layer->parent`:当前图层存在父图层(说明是子图层) 2. `layer->all_tasks_added`:当前图层所有绘制任务已添加完毕 3. `layer->draw_task_head == NULL`:当前图层自身无待处理任务 **操作逻辑**: - **搜索父图层中的“图层绘制任务”**: 遍历父图层的任务链表,寻找类型为 `LV_DRAW_TASK_TYPE_LAYER` 且状态为 `WAITING` 的任务。 - **匹配关联任务**: 检查任务的描述数据(`draw_dsc->src`)是否指向当前图层(即该任务是绘制当前子图层的任务)。 - **激活任务**: 将任务状态从 `WAITING` 改为 `QUEUED`,并通过 `lv_draw_dispatch_request()` 触发任务分派。 --- #### 二、**条件分支2:正常分派绘制任务到绘图单元** ```c else { // 正常分派任务到绘图单元(与之前问题中的逻辑一致) lv_draw_unit_t * u = _draw_info.unit_head; while(u) { int32_t taken_cnt = u->dispatch_cb(u, layer); if(taken_cnt != LV_DRAW_UNIT_IDLE) task_dispatched = true; u = u->next; } } ``` **触发条件**: 上述`if`条件不满足(即当前图层不是子图层,或仍有任务未处理)。 **操作逻辑**: 调用所有绘图单元(如CPU/GPU)的 `dispatch_cb` 回调函数,尝试分配绘制任务。若有单元接管任务,标记 `task_dispatched` 为 `true`。 --- ### 代码作用总结 1. **处理图层依赖**: 当子图层完成自身任务后,通知父图层中关联的“图层绘制任务”进入就绪状态,确保图层合成(Blending)正确执行。 2. **任务状态机管理**: 通过状态切换(`WAITING` → `QUEUED`)实现任务调度协作,避免任务重复执行或遗漏。 3. **资源协同**: 优先利用硬件加速单元(如GPU),若未处理则回退到CPU软渲染--- ### 后续流程 1. **返回值 `task_dispatched` 的影响**: - 若为 `true`:需等待绘图单元完成(如GPU异步渲染),完成后触发刷新事件。 - 若为 `false`:直接进入**图层混合阶段**,最终提交到显示缓冲区。 2. **典型应用场景**: - **复杂UI叠加**:多个图层叠加时(如弹出菜单+半透明背景),通过父-子图层任务协调完成渲染- **异步渲染优化**:GPU单元接管任务后,主线程可继续处理其他逻辑。 --- ### 代码LVGL中的位置 这段代码通常位于以下函数中: ```text lv_refr_area_part() → 在判断图层状态后执行 → 可能路径: 1. 处理父图层关联任务 → 触发混合 2. 分派到绘图单元 → 等待或继续渲染 ``` 可通过 `lv_refr.c` 文件中的 `lv_refr_area_part` 函数查看完整上下文。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值