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_COMPLEXif(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_COMPLEXif(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_COMPLEXdraw_shadow(coords, clip, dsc);
#endifdraw_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_COMPLEXelse {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;elsedraw_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搬运方式才有意义。