1.问题描述
TouchGFX的版本为V4.20,其新增了曲线控件的实现,但是最近分析发现当曲线的变化幅度比较大,或者曲线条数增加到4条以上之后便会出现曲线区域的闪烁,闪烁的现象大体可以归结如下:
各个通道是逐个绘制出来的;
完整的曲线区域会从上到下逐行显出;
每一次的刷新都相对比较慢;
2.问题分析
经过多种GUI的曲线绘制方法,我们大体可以归结为如下几个必备部分:
数据来源(这里不展开介绍);
添加点到曲线;
根据点计算连接线的绘制轮廓;
填充轮廓并于背景混合;
2.1.确认曲线的绘制顺序
按照我的理解,即使分行刷出,那么不应该是所有曲线一起绘制出来吗,而且所有区域加起来不应该就是每条曲线的绘制总时间吗,但是就结果来看时间似乎太长了。
于是我们就分析了一下GUI的绘制机制,大体可以归结为如下几步:
GUI上的控件通知GUI自己所在区域需要重刷;
GUI收到请求后对所有提交上来的区域进行取和;
GUI根据自己内部的算法将所有重绘区域进行排序和逐个重绘;
每次重绘都会遍历所有控件的Draw函数(实际上其不一定会在一个周期内全部绘制完成);
每个控件都会先判断自己所在区域与重绘区域是否有交集,如果有才会执行绘制函数;
绘制完成后,GUI删除该片脏区域,下个周期将不再重绘,直到有新的重绘请求到来;
根据以上的发现,我们可以解释如下:
GUI计算出单次最大能够重绘的区域大小导致了逐行;
由于我们的通道也是一个控件类,所以每个通道的Draw会被单独调用,这也就导致了逐个绘制;
如图1所示是通道绘制的Draw函数,我们可以看出其并没有关心Y方向的范围,所以每次无论绘制哪个区域,其都会将整个通道进行绘制,由画布完成目标区域的截取和填充,整体的示意图如图2所示。
所以我们可以认为影响性能的因素如下:
曲线区域被分的个数;
曲线区域的通道个数;
每个通道的绘制点数;
2.2.确认曲线的计算过程
但是以上依旧无法解释为什么单条绘制也很慢,并且我们通过Keil的调试对图1中的绘制执行进行了时间测量,我们发现这部分占据了整个绘制一大半时间。
我们继续分析图1中的计算过程,发现每次绘制都会从起始点走到终止点,然后又从终止点走回起始点,再结合其使用的画布原理,我们可以将其表示为如图3所示。
由于我们的曲线会有线宽,所以并不能简单地仅仅将两个点进行连接,所以需要通过在各个点的外围生成一个轮廓然后再对轮廓内的区间进行颜色填充,这样也就完成了一条曲线的绘制,这也就是为什么他要一去一回的原因。
那我们再来看看他的Draw函数里面又在计算什么,在开始之前我们还需要对图3的一个点进行放大,如图4所示,由于线宽的存在,我们需要以两个点的连线为中线进行展开,也就是中间的矩形区域就是我们要绘制的实际的线,外围的两个边(和中线平行)就是我们要的这两个点的轮廓。
知道了轮廓的形成原理,我们就要求解轮廓的各个点的坐标了,如图5所示,A点就是我们的第一个点,E点就是我们的第二个点,B点就是我们要求的第一个轮廓的坐标点。
同理求出下一个轮廓坐标,这也就求出了一个方向的,对于另一个方向在原理上我们是可以不求的,如图6所示,但是由于其是按照整数进行运算的,所以反过来的情况算出来的结果会有0~3个像素点的偏差。
2.3.确认曲线的绘制过程
前面的都已经拉到极致,那么就只能对画布动手了,我们对画布部分进行了计时,发现如下:
主要时间花在LineTo这个函数里,每次执行大概10us左右,但是每次都要执行上万次,这也就导致整体时间较长;
画布的提交到完全绘制出来并没有花费太多时间,并且每次绘制仅执行几次;
但是由于LintTo最终使用的是Outine这个类,而这个类的实现又没有开源,所以我们暂时无法修改,那么我们就只有两种选择:
将画布保存下来;
重构曲线的绘制函数,改为自己直接填充显存;
经过简单的验证,出去计算过程,直接将点打在显存里速度很快,但是GUI内部好像有机制能够识别哪些是他绘制的,不是用他的接口进行的绘制他好像不认,这也就导致方法2貌似不能直接使用。
3.问题总结
到此也就完成了开源部分的全部分析,优化方向大体包括如下:
通过循环内存缓存数据,对于新增点直接填充到缓存,不再从Nor中读取;
增加画布大小;
将通道和曲线以及栅格进行绘制合并;
循环内存中还要缓存轮廓的相关数据,避免重复计算;
直接打点显存,不再使用画布(这样也就不需要增加画布大小了);