当前位置: 首页 > news >正文

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

LVGL版本:8.1

往期回顾:

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

区域合并

       在前面的代码分析中,发现标记无效区域的工作其实很繁琐,虽然大部分区域因为包含关系被剔除,但仍可能存在相互交叉的区域,导致重复计算减少效率。因此在正式处理前,还需要根据情况选择性地合并这些交叉的区域。

       前面在标记无效区域时,将区域坐标及其数量都存储在inv_areas[]数组和inv_p这两个属性里,而还有一个属性inv_area_joined[]用于标明被合并掉的区域(数组元素值为1表示该索引对应的无效区域已被合并,后续刷新将略过它):

//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;  //无效区域数量uint8_t inv_area_joined[LV_INV_BUF_SIZE];  //数组,用于标明被合并的区域/*......*/
} lv_disp_t;

       具体合并算法过程如下面函数,其中用join_in下标表示合并主体区域,用join_from下标表示被合并区域:

//lv_refr.c
static void lv_refr_join_area(void)
{uint32_t join_from;uint32_t join_in;lv_area_t joined_area;/* 外循环,每次取一个合并主体区域 */for(join_in = 0; join_in < disp_refr->inv_p; join_in++) {if(disp_refr->inv_area_joined[join_in] != 0) continue;  //如果该区域已经被合并,则跳过/* 内循环,将其它被合并区域和外循环的合并主体区域进行比对,查看重叠情况 */for(join_from = 0; join_from < disp_refr->inv_p; join_from++) {if(disp_refr->inv_area_joined[join_from] != 0 || join_in == join_from) {continue;     //如果该区域是合并主体自身,或者该区域已经被合并,则跳过}/* 检查两块区域是否存在重叠 */if(_lv_area_is_on(&disp_refr->inv_areas[join_in], &disp_refr->inv_areas[join_from]) == false) {continue;}/* 生成一块更大的区域,使它刚好同时容纳两块重叠的区域 */_lv_area_join(&joined_area, &disp_refr->inv_areas[join_in], &disp_refr->inv_areas[join_from]);/* 只有当合成区域面积小于两块重叠区域面积之和,才将生成区域保存 */if(lv_area_get_size(&joined_area) < (lv_area_get_size(&disp_refr->inv_areas[join_in]) +lv_area_get_size(&disp_refr->inv_areas[join_from]))) {lv_area_copy(&disp_refr->inv_areas[join_in], &joined_area);/* 标记被合并的区域,下次遍历将跳过这块区域 */disp_refr->inv_area_joined[join_from] = 1;}}}
}

       其中的_lv_area_is_on()函数就是检查两块区域是否存在重叠:

       如果有重叠,_lv_area_join()函数会将两块重叠区域合成一个更大的区域。(或者这样描述:生成一个新的区域,使它刚好同时容纳两块重叠区域)。

       合成新区域后,还需要对比和之前两块重叠区域的面积进行比较,若小于两块面积之和,则将新区域覆盖到inv_area[join_in],并将inv_area_joined[join_from]置1,表示被合并(后续会被忽略),否则废弃。

       以上过程可以用下图来形象解释:

区域分块更新

       在注册显示器时,用于显示的缓存大小一般会设置为显示器像素总数,但有时候内存不足,可能仅设置了一个小缓存,同时无效区域的大小是有可能超出这个缓存大小的,因此在合并好区域后,又调用lv_refr_areas()对每个区域按行再进行一次分块刷新,具体分块原理后面会进一步说明。

//lv_refr.c
static void lv_refr_areas(void)
{px_num = 0;if(disp_refr->inv_p == 0) return;  //当前不存在无效区域,直接返回/* 找到最后一块要绘制的区域 */int32_t i;int32_t last_i = 0;for(i = disp_refr->inv_p - 1; i >= 0; i--) {if(disp_refr->inv_area_joined[i] == 0) {last_i = i;break;}}disp_refr->driver->draw_buf->last_area = 0;disp_refr->driver->draw_buf->last_part = 0;for(i = 0; i < disp_refr->inv_p; i++) {/* 更新合并后的区域 */if(disp_refr->inv_area_joined[i] == 0) {/* 最后一块合并区域,置位last_area标志 */if(i == last_i) disp_refr->driver->draw_buf->last_area = 1;disp_refr->driver->draw_buf->last_part = 0;  //在开始处理前清空last_part标志,该标志会在下面的函数完成后置位lv_refr_area(&disp_refr->inv_areas[i]);  //具体进行分块刷新的函数px_num += lv_area_get_size(&disp_refr->inv_areas[i]);  //累计区域像素数}}
}

        这里涉及到两个标志位,后续的解读会用“区域”来指代area,用“”来指代part

//lv_hal_disp.h
typedef struct _lv_disp_draw_buf_t {/*......*/volatile uint32_t last_area : 1;  /* 1表示最后一块区域正在被渲染*/volatile uint32_t last_part : 1;  /* 1表示该区域的最后一块正在被渲染*//*......*/
} lv_disp_draw_buf_t;

       上面函数针对每个合并后的区域,都使用lv_refr_area()进行分块刷新,具体分块的方法可以通俗解释为“在区域内等间距地画几条贯穿的横线分隔出几块”,即对区域的行数进行拆分,注意每次分块大小不能大于计算缓存(注册时传入,通常等于显示器总像素数),下面用图形解析不同的情况(绿色框代表显示器像素范围,红色框代表无效区域):

①渲染区域宽度超出显示器宽度(这通常不会发生,但为了减少可能存在的错误,依然考虑在内),此时单次刷新的最大高度被限制为max_row=buffer_size/area_width,根据区域高度大小有三种情况:

  • 区域高度很小

  • 区域高度很大但未超出显示器像素高度

  • 区域高度完全超出显示器像素高度

       注意上面第二、三点,最后一次刷新的分块高度,通常会小于理论的最大高度(max_row

②渲染区域宽高在显示器内,但显示缓存不足(小于显示器像素总数),也会发生和①类似的情况:

③渲染区域的宽度在显示器宽度内,但高度超出显示器高度(这通常也不会发生),此时截取掉超出的部分,仅渲染处于显示器内的区域,并一次性刷新完毕:

④渲染区域的宽高都在显示器范围内,也是一次性刷新:

        代码如下:

//lv_refr.c
static void lv_refr_area(const lv_area_t * area_p)
{/* 如果设置了全刷新,直接刷新全屏范围,并设置last_part为1表示最后一块 */if(disp_refr->driver->full_refresh) {lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);draw_buf->area.x1        = 0;draw_buf->area.x2        = lv_disp_get_hor_res(disp_refr) - 1;draw_buf->area.y1        = 0;draw_buf->area.y2        = lv_disp_get_ver_res(disp_refr) - 1;disp_refr->driver->draw_buf->last_part = 1;lv_refr_area_part(area_p);return;}/* 常规刷新方式: 分块刷新区域(重点) */lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);  //获取显示缓存buffer/* 计算单次刷新的最大行数 */lv_coord_t w = lv_area_get_width(area_p);   //待刷新区域的宽度lv_coord_t h = lv_area_get_height(area_p);   //待刷新区域的高度lv_coord_t y2 = area_p->y2 >= lv_disp_get_ver_res(disp_refr) ?lv_disp_get_ver_res(disp_refr) - 1 : area_p->y2;  //下界不能超出屏幕int32_t max_row = (uint32_t)draw_buf->size / w;  //size就是显示器的总像素数if(max_row > h) max_row = h;  //max_row就是分块后,单次刷新的最大行数/* 舍入操作(如有定义) */if(disp_refr->driver->rounder_cb) {lv_area_t tmp;tmp.x1 = 0;tmp.x2 = 0;tmp.y1 = 0;lv_coord_t h_tmp = max_row;do {tmp.y2 = h_tmp - 1;disp_refr->driver->rounder_cb(disp_refr->driver, &tmp);/* 如果舍入结果小于max_row,则符合条件,跳出 */if(lv_area_get_height(&tmp) <= max_row) break;/* 减少分块高度,以匹配显示器的舍入规则 */h_tmp--;} while(h_tmp > 0);if(h_tmp <= 0) {LV_LOG_WARN("Can't set draw_buf height using the round function. (Wrong round_cb or to ""small draw_buf)");return;}else {max_row = tmp.y2 + 1;}}/* 在direct模式下,所有缓存将直接绘制到绝对坐标位置上 */if(disp_refr->driver->direct_mode) {draw_buf->area.x1 = 0;draw_buf->area.x2 = lv_disp_get_hor_res(disp_refr) - 1;draw_buf->area.y1 = 0;draw_buf->area.y2 = lv_disp_get_ver_res(disp_refr) - 1;disp_refr->driver->draw_buf->last_part = disp_refr->driver->draw_buf->last_area;lv_refr_area_part(area_p);}else {/* 常规刷新模式下,从给定区域的起始位置开始分块刷新 */lv_coord_t row;lv_coord_t row_last = 0;for(row = area_p->y1; row + max_row - 1 <= y2; row += max_row) {/* 计算下一块的起始和结束行数 */draw_buf->area.x1 = area_p->x1;draw_buf->area.x2 = area_p->x2;draw_buf->area.y1 = row;draw_buf->area.y2 = row + max_row - 1;if(draw_buf->area.y2 > y2) draw_buf->area.y2 = y2;row_last = draw_buf->area.y2;if(y2 == row_last) disp_refr->driver->draw_buf->last_part = 1;  //y2正好时最后一行lv_refr_area_part(area_p);}/* y2是行下限,上面的均匀分块可能会导致最后一次遍历错过一些行,在这里要补上 */if(y2 != row_last) {draw_buf->area.x1 = area_p->x1;draw_buf->area.x2 = area_p->x2;draw_buf->area.y1 = row;draw_buf->area.y2 = y2;  //将y2作为底部disp_refr->driver->draw_buf->last_part = 1;lv_refr_area_part(area_p);}}
}

       在每次分块完成后,都是使用lv_refr_area_part()函数进一步处理,该函数里需要先找到能完全覆盖该无效区域的最上层对象(子对象总是默认放置在父对象的上层),

//lv_refr.c
static void lv_refr_area_part(const lv_area_t * area_p)
{lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);/* 单一缓存下,当缓存正在刷写到屏幕,需要等待直至缓存释放 */if(draw_buf->buf1 && !draw_buf->buf2) {while(draw_buf->flushing) {  //flushing标志置位表示正在刷写,需要调用lv_disp_flush_ready()进行清除if(disp_refr->driver->wait_cb) disp_refr->driver->wait_cb(disp_refr->driver);}}lv_obj_t * top_act_scr = NULL;lv_obj_t * top_prev_scr = NULL;/* 取无效区域和活动屏幕的交集 */lv_area_t start_mask;_lv_area_intersect(&start_mask, area_p, &draw_buf->area);/* 获取活跃屏幕内,能完全覆盖无效区域的最上层对象(后创建的在上层) */top_act_scr = lv_refr_get_top_obj(&start_mask, lv_disp_get_scr_act(disp_refr));if(disp_refr->prev_scr) {  //寻找上一帧是否存在上述类型的对象top_prev_scr = lv_refr_get_top_obj(&start_mask, disp_refr->prev_scr);}/* 如果不存在能完全覆盖该区域的对象,则先绘制背景 */if(top_act_scr == NULL && top_prev_scr == NULL) {if(disp_refr->bg_fn) {disp_refr->bg_fn(&start_mask);} else if(disp_refr->bg_img) {lv_draw_img_dsc_t dsc;lv_draw_img_dsc_init(&dsc);dsc.opa = disp_refr->bg_opa;lv_img_header_t header;lv_res_t res;res = lv_img_decoder_get_info(disp_refr->bg_img, &header);if(res == LV_RES_OK) {lv_area_t a;lv_area_set(&a, 0, 0, header.w - 1, header.h - 1);lv_draw_img(&a, &start_mask, disp_refr->bg_img, &dsc);}else {LV_LOG_WARN("Can't draw the background image");}}else {lv_draw_rect_dsc_t dsc;lv_draw_rect_dsc_init(&dsc);dsc.bg_color = disp_refr->bg_color;dsc.bg_opa = disp_refr->bg_opa;lv_draw_rect(&start_mask, &start_mask, &dsc);}}/* 刷新上一帧屏幕,仅动画使用 */if(disp_refr->prev_scr) {/* 获取上一帧该区域未被遮挡的上层对象 */if(top_prev_scr == NULL) {top_prev_scr = disp_refr->prev_scr;}/* 对该对象进行刷新 */lv_refr_obj_and_children(top_prev_scr, &start_mask);}if(top_act_scr == NULL) {top_act_scr = disp_refr->act_scr;}/* 刷新活跃屏幕(act_scr)上对应的对象 */lv_refr_obj_and_children(top_act_scr, &start_mask);/* 无条件地刷新顶层和系统层屏幕 */lv_refr_obj_and_children(lv_disp_get_layer_top(disp_refr), &start_mask);lv_refr_obj_and_children(lv_disp_get_layer_sys(disp_refr), &start_mask);/* 在双缓冲模式下,仅在所有区域重新渲染完毕后,进行一次绘制(flush)* 普通模式下,每个区域的更新都会进行一次绘制 */if(disp_refr->driver->full_refresh == false) {draw_buf_flush();}
}

       层层解剖,最后使用lv_refr_obj_and_children()函数进行更新。该函数除了要刷新上面说的完全覆盖区域的上层对象,还要更新其“弟弟”节点,因为这些“弟弟”们也可以覆盖在该对象上,进一步的,还要更新该对象的父对象的“弟弟”节点,并一直向上寻找直至顶层屏幕结束。如下图,粉色节点为上层对象,橙色节点为同样需要更新的“弟弟”或“叔叔”节点,绿色节点为这些待更新节点的子节点(会被一同刷新)。

//lv_refr.c
static void lv_refr_obj_and_children(lv_obj_t * top_p, const lv_area_t * mask_p)
{/*Normally always will be a top_obj (at least the screen)*but in special cases (e.g. if the screen has alpha) it won't.*In this case use the screen directly*/if(top_p == NULL) top_p = lv_disp_get_scr_act(disp_refr);if(top_p == NULL) return;  /*Shouldn't happen*//* 刷新该对象及其子对象 */lv_refr_obj(top_p, mask_p);/* 接下来的操作都是刷新该节点的“弟弟”节点(后创建但同属一个父节点) */lv_obj_t * par;lv_obj_t * border_p = top_p;par = lv_obj_get_parent(top_p);/* 寻找“弟弟”节点 */while(par != NULL) {bool go = false;uint32_t i;uint32_t child_cnt = lv_obj_get_child_cnt(par);for(i = 0; i < child_cnt; i++) {lv_obj_t * child = par->spec_attr->children[i];if(!go) {if(child == border_p) go = true;  //标记自身,之后遍历的都是“弟弟”了}else {lv_refr_obj(child, mask_p);}}/* 调用父节点的“后期绘制事件”(DRAW_POST)相关回调函数 */lv_event_send(par, LV_EVENT_DRAW_POST_BEGIN, (void *)mask_p);lv_event_send(par, LV_EVENT_DRAW_POST, (void *)mask_p);lv_event_send(par, LV_EVENT_DRAW_POST_END, (void *)mask_p);/* 对父节点同样执行一遍上述操作,直至没有下一个父节点(顶层屏幕) */border_p = par;par = lv_obj_get_parent(par);}
}

       上面的迭代,每次遍历一个对象,都使用lv_refr_obj()函数来更新其自身及其所有子孙节点。该函数就是更新区域的最后一步了,用于更新传入的节点,并对所有子孙节点做同样的递归更新操作。

//lv_refr.c
static void lv_refr_obj(lv_obj_t * obj, const lv_area_t * mask_ori_p)
{/* 不刷新隐藏的节点对象 */if(lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return;bool union_ok; /* 传入对象的坐标区域和无效区域的子集 */lv_area_t obj_mask;lv_area_t obj_ext_mask;lv_area_t obj_area;lv_coord_t ext_size = _lv_obj_get_ext_draw_size(obj);  //获取绘制阶段的额外范围lv_obj_get_coords(obj, &obj_area);  //获取对象坐标范围obj_area.x1 -= ext_size;obj_area.y1 -= ext_size;obj_area.x2 += ext_size;obj_area.y2 += ext_size;union_ok = _lv_area_intersect(&obj_ext_mask, mask_ori_p, &obj_area);/* 仅当对象主绘制区域和传入的无效区域存在交集时才进行绘制 */if(union_ok != false) {/* 主绘制阶段 */lv_event_send(obj, LV_EVENT_DRAW_MAIN_BEGIN, &obj_ext_mask);lv_event_send(obj, LV_EVENT_DRAW_MAIN, &obj_ext_mask);lv_event_send(obj, LV_EVENT_DRAW_MAIN_END, &obj_ext_mask);/* 去掉主绘制阶段的额外范围,进行子节点绘制(子节点无法看到这些范围) */lv_obj_get_coords(obj, &obj_area);union_ok = _lv_area_intersect(&obj_mask, mask_ori_p, &obj_area);if(union_ok != false) {lv_area_t mask_child; /*Mask from obj and its child*/lv_area_t child_area;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];lv_obj_get_coords(child, &child_area);ext_size = _lv_obj_get_ext_draw_size(child);child_area.x1 -= ext_size;child_area.y1 -= ext_size;child_area.x2 += ext_size;child_area.y2 += ext_size;/* 获取子节点和无效区域的交集 */union_ok = _lv_area_intersect(&mask_child, &obj_mask, &child_area);/* 如果有交集则递归调用该函数刷新子节点 */if(union_ok) {/* 刷新下一个子节点 */lv_refr_obj(child, &mask_child);}}}/* 当所有子节点都绘制完毕,进入“后期绘制”阶段 */lv_event_send(obj, LV_EVENT_DRAW_POST_BEGIN, &obj_ext_mask);lv_event_send(obj, LV_EVENT_DRAW_POST, &obj_ext_mask);lv_event_send(obj, LV_EVENT_DRAW_POST_END, &obj_ext_mask);}
}

       到这里才发现,最终遍历到具体的对象和区域后,会发送DRAW相关事件给该对象,而更新的关键就在于DRAW事件的相关回调函数。

       到这里回过头捋一下更新任务内部实现的调用路径:

--> _lv_disp_refr_timer(tmr),更新任务主体

     --> lv_obj_update_layout(screen),布局更新,将脏数据进一步转化成无效区域

     --> lv_refr_join_area(),合并重叠的无效区域

      --> lv_refr_areas(),遍历无效区域进行更新

            --> lv_refr_area(area),将每一片无效区域进一步分块

                  --> lv_refr_part(area),找到分块后,完全覆盖该块的最顶层元素进行遍历

                       --> lv_refr_obj_and_children(top_obj, area),遍历最顶层元素及其“弟弟”节点们

                            --> lv_refr_obj(top_obj, area),向需要更新的对象发送draw相关事件

     --> draw_buf_flush(),将缓存刷写到显示器

       下一片文章,将分析draw事件回调函数如何重绘对象并最终刷写缓冲。

http://www.xdnf.cn/news/361351.html

相关文章:

  • 电厂数据库未来趋势:时序数据库 + AI 驱动的自优化系统
  • 期货跟单软件如何对实盘进行风控?
  • go语言封装、继承与多态:
  • 【A2A】管中窥豹,google源码python-demo介绍
  • Go语言中 源文件开头的 // +build 注释的用法
  • 母亲节祝福网页制作
  • 推荐一个很方便的浏览器管理插件Wetab插件
  • 水印云:AI赋能,让图像处理变得简单高效
  • VSCode如何解决打开html页面中文乱码的问题
  • 工业软件自主化突围:RTOS 如何打破 “协议栈 - 控制器” 生态垄断
  • 零件画图实战提升案例(上)
  • 企业高性能WEB服务器—Nginx
  • 【论文阅读】基于客户端数据子空间主角度的聚类联邦学习分布相似性高效识别
  • 深度解析动态IP业务核心场景:从技术演进到行业实践
  • 住宅IP的深度解析与合理运用
  • 探索Stream流:高效数据处理的秘密武器
  • TOGAF 企业架构介绍(4A架构)
  • [javascript]取消异步请求
  • 26考研——中央处理器_指令执行过程(5)
  • qiankun微前端任意位置子应用
  • Kubernetes调度策略深度解析:NodeSelector与NodeAffinity的正确打开方式
  • 网络安全体系架构:核心框架与关键机制解析
  • kubernetes服务自动伸缩-HPA
  • C++ 访问者模式详解
  • Redis面试题
  • 力扣26——删除有序数组中的重复项
  • 【推荐笔记工具】思源笔记 - 隐私优先的个人知识管理系统,支持 Markdown 排版、块级引用和双向链接
  • Qt 的原理及使用(1)——qt的背景及安装
  • 在另一个省发布抖音作品,IP属地会随之变化吗?
  • 【数据结构】1. 时间/空间复杂度