LVGL源码学习之渲染、更新过程(3)---绘制和刷写

LVGL版本:8.1

往期回顾:

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

LVGL源码学习之渲染、更新过程(2)---无效区域的处理

       前文提到,在处理完无效区域后,会得到一个个需要重新绘制的对象,这些对象将在DRAW事件中进行重绘,并将结果写入显示缓存中。

 对象重绘

       来看看draw事件的回调函数都做了什么。在draw主绘制阶段后期绘制阶段,都会进入lv_obj_draw()函数:

//lv_obj.c
static void lv_obj_draw(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t * obj = lv_event_get_target(e);  //被更新的对象
    if(code == LV_EVENT_COVER_CHECK) {
        /*......*/
    }
    else if(code == LV_EVENT_DRAW_MAIN) {  //主绘制阶段
        const lv_area_t * clip_area = lv_event_get_param(e);
        lv_draw_rect_dsc_t draw_dsc;
        lv_draw_rect_dsc_init(&draw_dsc);  //初始化绘制描述符
        /* 是否需要后期再绘制边框 */
        if(lv_obj_get_style_border_post(obj, LV_PART_MAIN)) {
            draw_dsc.border_post = 1;
        }
        //把对象的样式填进绘制描述符
        lv_obj_init_draw_rect_dsc(obj, LV_PART_MAIN, &draw_dsc);
        //被更新对象的坐标范围(包含transform带来的变化)
        lv_coord_t w = lv_obj_get_style_transform_width(obj, LV_PART_MAIN);
        lv_coord_t h = lv_obj_get_style_transform_height(obj, LV_PART_MAIN);
        lv_area_t coords;
        lv_area_copy(&coords, &obj->coords);
        coords.x1 -= w;
        coords.x2 += w;
        coords.y1 -= h;
        coords.y2 += h;

        lv_obj_draw_part_dsc_t part_dsc;
        lv_obj_draw_dsc_init(&part_dsc, clip_area);  //将无效区域填装进来
        part_dsc.class_p = MY_CLASS;
        part_dsc.type = LV_OBJ_DRAW_PART_RECTANGLE;
        part_dsc.rect_dsc = &draw_dsc;
        part_dsc.draw_area = &coords;
        part_dsc.part = LV_PART_MAIN;
        lv_event_send(obj, LV_EVENT_DRAW_PART_BEGIN, &part_dsc);

        lv_draw_rect(&coords, clip_area, &draw_dsc);  //开始绘制

        lv_event_send(obj, LV_EVENT_DRAW_PART_END, &part_dsc);

#if LV_DRAW_COMPLEX
        if(lv_obj_get_style_clip_corner(obj, LV_PART_MAIN)) {
            /*If the radius is 0 the parent's coordinates will clip anyway*/
            lv_coord_t r = lv_obj_get_style_radius(obj, LV_PART_MAIN);
            if(r != 0) {
                lv_draw_mask_radius_param_t * mp = lv_mem_buf_get(sizeof(lv_draw_mask_radius_param_t));
                lv_draw_mask_radius_init(mp, &obj->coords, r, false);
                /*Add the mask and use `obj+8` as custom id. Don't use `obj` directly because it might be used by the user*/
                lv_draw_mask_add(mp, obj + 8);
            }
        }
#endif
    }
    else if(code == LV_EVENT_DRAW_POST) {  //后期绘制阶段
        const lv_area_t * clip_area = lv_event_get_param(e);
        draw_scrollbar(obj, clip_area);

#if LV_DRAW_COMPLEX
        if(lv_obj_get_style_clip_corner(obj, LV_PART_MAIN)) {
            lv_draw_mask_radius_param_t * param = lv_draw_mask_remove_custom(obj + 8);
            if(param) {
                lv_draw_mask_free_param(param);
                lv_mem_buf_release(param);
            }
        }
#endif

        /*If the border is drawn later disable loading other properties*/
        if(lv_obj_get_style_border_post(obj, LV_PART_MAIN)) {
            lv_draw_rect_dsc_t draw_dsc;
            lv_draw_rect_dsc_init(&draw_dsc);
            draw_dsc.bg_opa = LV_OPA_TRANSP;
            draw_dsc.outline_opa = LV_OPA_TRANSP;
            draw_dsc.shadow_opa = LV_OPA_TRANSP;
            draw_dsc.bg_img_opa = LV_OPA_TRANSP;
            lv_obj_init_draw_rect_dsc(obj, LV_PART_MAIN, &draw_dsc);

            lv_coord_t w = lv_obj_get_style_transform_width(obj, LV_PART_MAIN);
            lv_coord_t h = lv_obj_get_style_transform_height(obj, LV_PART_MAIN);
            lv_area_t coords;
            lv_area_copy(&coords, &obj->coords);
            coords.x1 -= w;
            coords.x2 += w;
            coords.y1 -= h;
            coords.y2 += h;

            lv_obj_draw_part_dsc_t part_dsc;
            lv_obj_draw_dsc_init(&part_dsc, clip_area);
            part_dsc.class_p = MY_CLASS;
            part_dsc.type = LV_OBJ_DRAW_PART_BORDER_POST;
            part_dsc.rect_dsc = &draw_dsc;
            part_dsc.draw_area = &coords;
            part_dsc.part = LV_PART_MAIN;
            lv_event_send(obj, LV_EVENT_DRAW_PART_BEGIN, &part_dsc);

            lv_draw_rect(&coords, clip_area, &draw_dsc);
            lv_event_send(obj, LV_EVENT_DRAW_PART_END, &part_dsc);
        }
    }
}

       主绘制函数lv_draw_rect(),用于搭建主体框架,包括位置、大小,以及使用蒙版(masking)来勾勒圆角(radius)、实现透明度等。

//lv_draw_rect.c
void lv_draw_rect(const lv_area_t * coords, const lv_area_t * clip, const lv_draw_rect_dsc_t * dsc)
{
    if(lv_area_get_height(coords) < 1 || lv_area_get_width(coords) < 1) return;
#if LV_DRAW_COMPLEX
    draw_shadow(coords, clip, dsc);
#endif

    draw_bg(coords, clip, dsc);  //绘制背景
    draw_bg_img(coords, clip, dsc);  //绘制背景图片
    draw_border(coords, clip, dsc);  //绘制边框(如前面设置border_post,则此处会返回)
    draw_outline(coords, clip, dsc);  //绘制外轮廓

    LV_ASSERT_MEM_INTEGRITY();
}

       上面涉及所有的绘制,如背景、图片、边框、外轮廓等,底层都是调用_lv_blend_fill()函数将内容写入显示缓存中。

//lv_draw_blend.c
/**
 * Fill and area in the display buffer.
 * @param clip_area 无效区域在当前对象范围的子集(绝对坐标)
 * @param fill_area 对象的坐标范围(绝对坐标)
 * @param color 背景颜色
 * @param mask a mask to apply on the fill (uint8_t array with 0x00..0xff values).
 *             Relative to fill area but its width is truncated to clip area.
 * @param mask_res LV_MASK_RES_COVER: the mask has only 0xff values (no mask),
 *                 LV_MASK_RES_TRANSP: the mask has only 0x00 values (full transparent),
 *                 LV_MASK_RES_CHANGED: the mask has mixed values
 * @param opa 背景透明度(0-255)
 * @param mode blend mode from `lv_blend_mode_t`
 */
LV_ATTRIBUTE_FAST_MEM void _lv_blend_fill(const lv_area_t * clip_area, const lv_area_t * fill_area,
                                          lv_color_t color, lv_opa_t * mask, lv_draw_mask_res_t mask_res, lv_opa_t opa,
                                          lv_blend_mode_t mode)
{
    /*Do not draw transparent things*/
    if(opa < LV_OPA_MIN) return;
    if(mask_res == LV_DRAW_MASK_RES_TRANSP) return;

    lv_disp_t * disp = _lv_refr_get_disp_refreshing();
    lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp);
    const lv_area_t * disp_area = &draw_buf->area;  //无效区域的坐标
    lv_color_t * disp_buf = draw_buf->buf_act;

    if(disp->driver->gpu_wait_cb) disp->driver->gpu_wait_cb(disp->driver);

    /* 获取实际绘制区域,通常小于等于clip_area(之前传来的无效区域的子集) */
    lv_area_t draw_area;
    if(!_lv_area_intersect(&draw_area, clip_area, fill_area)) return;

    /* draw_area是绝对坐标,需要转化成无效区域内的相对坐标 */
    lv_area_move(&draw_area, -disp_area->x1, -disp_area->y1);

    /*Round the values in the mask if anti-aliasing is disabled*/
    if(mask && disp->driver->antialiasing == 0 && mask) {
        int32_t mask_w = lv_area_get_width(&draw_area);
        int32_t i;
        for(i = 0; i < mask_w; i++)  mask[i] = mask[i] > 128 ? LV_OPA_COVER : LV_OPA_TRANSP;
    }
    /* 填充显示缓存disp_buf */
    if(disp->driver->set_px_cb) {
        fill_set_px(disp_area, disp_buf, &draw_area, color, opa, mask, mask_res);
    }
    else if(mode == LV_BLEND_MODE_NORMAL) {
        fill_normal(disp_area, disp_buf, &draw_area, color, opa, mask, mask_res);
    }
#if LV_DRAW_COMPLEX
    else {
        fill_blended(disp_area, disp_buf, &draw_area, color, opa, mask, mask_res, mode);
    }
#endif
}

刷写显示器

       做完了所有的渲染和绘制,回到最初的更新任务,接下来要做的就是把写好的缓存刷写到显示器上,具体函数为draw_buf_flush()

//lv_refr.c
static void draw_buf_flush(void)
{
    lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);
    lv_color_t * color_p = draw_buf->buf_act;

    /*Flush the rendered content to the display*/
    lv_disp_t * disp = _lv_refr_get_disp_refreshing();
    if(disp->driver->gpu_wait_cb) disp->driver->gpu_wait_cb(disp->driver);

    /* 双缓冲模式下,需要等待另一个缓冲被释放(相当于上一轮刷写完成),并调用等待函数 */
    if(draw_buf->buf1 && draw_buf->buf2) {
        while(draw_buf->flushing) {
            if(disp_refr->driver->wait_cb) disp_refr->driver->wait_cb(disp_refr->driver);
        }
    }

    draw_buf->flushing = 1;

    if(disp_refr->driver->draw_buf->last_area && disp_refr->driver->draw_buf->last_part) draw_buf->flushing_last = 1;
    else draw_buf->flushing_last = 0;  //该标志位只有当最后一片区域的最后一块渲染完成后,才能在刷写过程中置位

    if(disp->driver->flush_cb) {
        /* 显示旋转需要进行缓冲区数据转换,该函数内已经包括了刷写功能 */
        if(disp->driver->rotated != LV_DISP_ROT_NONE && disp->driver->sw_rotate) {
            draw_buf_rotate(&draw_buf->area, draw_buf->buf_act);
        }
        else {
            call_flush_cb(disp->driver, &draw_buf->area, color_p);  //正常刷写
        }
    }
    if(draw_buf->buf1 && draw_buf->buf2) {  //双缓冲下的缓冲区切换
        if(draw_buf->buf_act == draw_buf->buf1)
            draw_buf->buf_act = draw_buf->buf2;
        else
            draw_buf->buf_act = draw_buf->buf1;
    }
}

       底层调用注册显示器传入的flush回调函数,刷写到显示器上。

//lv_refr.c
static void call_flush_cb(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{
    REFR_TRACE("Calling flush_cb on (%d;%d)(%d;%d) area with %p image pointer", area->x1, area->y1, area->x2, area->y2,
               (void *)color_p);

    lv_area_t offset_area = {
        .x1 = area->x1 + drv->offset_x,
        .y1 = area->y1 + drv->offset_y,
        .x2 = area->x2 + drv->offset_x,
        .y2 = area->y2 + drv->offset_y
    };

    drv->flush_cb(drv, &offset_area, color_p);  //刷写硬件的函数
}

       在前面的代码里,根据更新模式不同,采取了不同的刷写方式:

  • 直写模式(direct)下,不分块,但每片区域渲染完毕后都会进行一次刷写;
  • 全刷新模式(full_refresh)下,完成屏幕范围内的重新渲染后,一次性将显示缓存刷写到显示器;
  • 普通模式(normal)下,分块进行刷新,每块完成后都进行一次刷写;

总结

       终于研究完了整个流程,原理倒是不难懂,但代码总感觉不是很漂亮,特别多冗余的代码(为了健壮性考虑,在后面的版本,多少都精简了),比如经常要取无效区域的当前对象坐标区域的交集,这种操作几乎每一层函数调用都在发生,看多了容易理得很乱。

       在测试程序时,特地测试了不同的更新模式和缓冲情况下的性能表现:

  • 全刷新模式下,使用大缓冲(或者显示器大小的缓冲),渲染速度较慢,反而使用小缓冲,速度更快,代价是容易花屏,但整体速度都不如普通模式;
  • 直写模式下,必须使用显示器大小缓冲,否则会报错,速度上略慢于普通模式,但差距很小;
  • 普通模式下,速度最快,也不会有花屏现象,可以适应不同大小的缓冲,只要不是特别大或特别小,性能差距很小;

       对于是否使用单双缓冲,主要取决于内存大小以及刷写方式,如果刷写方式为CPU直接复制内存,则失去了使用双缓冲的意义,双缓存得配合DMA搬运方式才有意义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值