【玩转Arm-2D】如何使用脏矩阵优化帧率(基础篇)

image.png

【说在前面的话】


很多朋友可能都曾惊叹于此前的一段Cortex-M0+处理器在25MHz系统频率下“仅凭一己之力”拖着宛如细狗一般带宽的SPI屏幕狂飙直方图的视频:
image.png

值得强调的是,在这一系统中:

  • Cortex-M0+ 实时地将背景图片圆角矩形半透明方框和位于前景的直方图合成到了一起;
  • 该实例未使用任何图形加速硬件,甚至连SPI也是CPU自己去填充数据的(未借助DMA);
  • 虽然使用了最新的Arm Compiler 6,但优化等级使用的却不是“以最大化牺牲代码尺寸为代价换取最大化性能”的-Omax——相反,这里用到的是“追求尺寸和性能相平衡”的-OsLink Time Optimization
  • 除去背景图片后的代码尺寸为40K Flash,项目整体SRAM占用为12K(其中还包含 3K STACK1K HEAP);PFB的尺寸为 320*8

无论此前是否接触过Arm-2D,上述环境数据与最终达成的视觉效果让很多朋友一脸懵逼。

那么,这背后究竟隐藏着怎样的黑科技呢?其实,对大多数朋友来说,要理解这一切,一张图足以:
image.png

这是前述例子在开启脏矩阵调试模式后的效果(为了方便观察,我们将直方图的颜色改成了灰色),这里可以清晰的看到:虽然在我们肉眼中画面在较短时间内呈现巨大变化,但相对前一帧,改变的内容就只有红色方框所标记的那一小部分而已——这就可以合理的解释为什么在LCD带宽较低CPU性能较弱(且未开启编译器最高性能优化)的情况下,我们仍然可以获得流畅的动态画面。

是的,这就是脏矩阵技术的魅力。

【什么是脏矩阵】


脏矩阵不是什么新兴的科技,它的基本思路也非常的简单,即:只在需要的时候重绘画面中变化的部分。由于基于光栅的绘图技术在数据结构上总表现为一个矩形区域,且画面中变化的内容往往又被称为“弄脏了”的部分——“脏矩阵”故此得名。

脏矩阵技术的应用非常广泛,例如我们所熟悉的Windows系统,大部分时候就只会更新鼠标指针滑动所经过的那一小片矩形区域而已。

正如前面所说,因为脏矩阵在降低传输带宽和CPU占用方面有着不可替代的优势,几乎所有的知名GUI协议栈都在默认情况下悄悄地使用各种各样的脏矩阵算法对系统帧率进行优化。

从原理角度出发,一个脏矩阵算法本质上需要回答以下两个问题,即:

  1. 画面上有哪些内容需要重绘
  2. 这些区域何时需要重绘

其中,第一个问题还可以进一步细化为:如何设置矩形区域对需要重绘的内容进行覆盖。例如:究竟是用很多细小的矩形区域来“精确制导”还是用少数几个较大的矩形来“火力覆盖”。

如果你对上述问题感到困惑,不妨来看一个简单且极端的例子:

image.png

如上图所示,假设相对前一帧,屏幕上突然出现了一个横跨对角的直线,很多较为初级的脏矩阵算法可能会“无奈”的做出将屏幕完全重绘的决策——而在我们看来,只因为1%都不到的像素变化就重绘整个屏幕,显示然是不合理的。

受制于脏矩阵必须是矩形区域的数据结构限制,我们其实还可以通过下面的方法来缩小重绘的面积:

image.png

有些朋友可能立马会提出这样的疑问:可不可以用一连串宽度(Width)和高度(Height)都为1个像素的脏矩阵来精确逼近整个斜线呢?理论上可行,但从时间成本角度出发,这种做法却往往不是最优解。原因有三:

  • 一个可以根据目标内容自己“想出”最优的脏矩阵拆分策略的智能算法,其时间和空间开销通常都不可小觑;
  • 底层的每一次脏矩阵刷新都有固定的开支,即便你拥有完整的显示缓冲(Framebuffer)保存着当前帧的所有像素,用 1*1的脏矩阵来覆盖一条斜线就意味着将这一固定开支直接放大数百倍(具体取决于这条斜线所涉及的像素数量)——在把这一固定成本考虑进去后,很可能1*1的极限逼近方案就已经不是性能上的最优解了;
  • 在无法使用完整显示缓冲(Full Framebuffer)的环境下,Partial Framebuffer(PFB) 就成了唯一的选择。而PFB的实现原理不可避免的涉及“对画面内容的重复绘制”——对每一次绘制来说,哪怕当前PFB区域以外的部分都会被忽略,但这一过程中“用户代码的执行”和“区域的裁剪”仍然伴随着不可忽略的时间开销——当使用1*1的极限逼近方案时,就意味着同样的画面绘制函数会被重复上百次——最终的时间成本就会积累到不可接受的程度。

从这个例子可以看出,对于需要重绘的区域,如何合理的设置脏矩阵——在性能收益与时间成本之间做权衡——是一个颇具挑战的难题

脏矩阵本身虽然可以有效降低刷新区域面积——提高帧率,但生成脏矩阵的算法本身却可能消耗巨大。这里的矛盾在于:太简陋的算法会因为一条对角线就更新整个屏幕;而“想的太多”的算法也可能会因为时间成本的积累而把脏矩阵的带来的收益抵消了不少——难就难在一个度的把握上。

那么Arm-2D是如何应对这一难题的呢?

【Arm-2D的脏矩阵策略】


和很多人的猜测不同,Arm-2D并不会被上述“追求权衡”的逻辑缠绕进去,而是站在更高的一个维度重新解构这一难题,其“心路历程”如下:

  • 为什么生成脏矩阵的算法消耗巨大?

因为它需要足够聪明。

  • 为什么它需要足够聪明?

因为它并不真的知道所需更新的内容究竟是什么,而只能依据像素的坐标信息进行分析和猜测。

  • 为什么它不知道所需更新的内容是什么?

因为这一信息掌握在创建画面的用户手里。

  • 既然关键信息掌握在用户手里,为什么不直接让用户去设置脏矩阵方案呢?

很好!出院!

Arm-2D也是这么想的。

我们前面说过,Arm-2D本身并不是GUI(缺乏正常GUI所必须的控件管理、消息处理、控件库和设计器)——只有在你的芯片资源负担不起一个正常GUI协议栈的情况下,才推荐使用Arm-2D直接开发图形应用。在这一前提条件下,其实并非Arm-2D偷懒,而是那种“用户你只管绘图,最小更新区域我来猜”的“智能友好”算法压根就是负担不起的

事实上,让掌握应用信息的一方去设置脏矩阵区域(而非建立一个智能算法去揣测用户的实际意图)是嵌入式行业的通解


image.png

以普通GUI为例,当用户通过某种方式(比如设计器、数据结构或者API调用)将控件之间的隶属和层次关系告知GUI协议栈后,负责控件树管理的服务就掌握了所有的应用信息。这里,控件哪怕看起来与一个圆角矩形无异,它本身也包含着额外的应用信息——比如当你按下一个按钮的时候,协议栈知道只有边框的阴影部分需要重绘,而按钮的表面却无需更新。正因如此,GUI协议栈可以根据用户与控件的交互轻松地生成最优的脏矩阵覆盖方案,而无需什么智能算法的加持——掌握信息的一方直接设置脏矩阵不存在信息差,自然也不存在“猜测”的成本


当我们无法负担一个普通GUI协议栈的成本时,用户除了要直接使用Arm-2D所提供的图形API来绘制界面,还应该(根据自己对界面的设计理解)承担起设置脏矩阵的职能

那么?让用户自己手动设置脏矩阵容易么?不必担心,从信息的角度来说,实际过程不应该非常复杂,原因有二:

  • 如前面所说,用户原本就对自己设计的界面了如指掌——哪里需要更新、哪里保持不变根本无需过多得思考
  • 在《【玩转Arm-2D】Arm-2D应用开发入门》中,我们介绍过一种基于面板还状态拆分的界面设计范式。借助场景播放器的帮助,复杂的界面可以被拆分成“代表不同状态的简单面板”——正因为每个面板都很简单,要覆盖更新区域所需的脏矩阵配置也不会复杂

image.png

正因如此,为了满足用户不同场景下不同层次的需求、为了最大限度提供傻瓜化的脏矩阵描述方式,Arm-2D提供以下的方案:

  • 静态脏矩阵
  • 动态脏矩阵傻瓜化服务模块arm\_2d\_helper\_dirty\_region
  • 旋转和缩放脏矩阵服务模块(arm\_2d\_helper\_transform
  • 自定义动态脏矩阵arm\_2d\_dynamic\_dirty\_region

本着“从易到难、循序渐进”的原则,我们将结合实例,以多个篇幅深入浅出地为大家介绍Arm-2D下的脏矩阵技术。

【针对固定区域静态脏矩阵】

在一些典型的场景中,往往只有少数固定区域需要经常性的进行更新。比如,下面视频中“负责时间显示的区域”以及“负责心率波形显示的区域”,都是需要经常性内容更新的固定区域。

image.png

即,下图中红色方框所标注的区域:

image.png

Arm-2D的术语体系中,我们将这类“坐标位置和尺寸大小都是固定不变”的脏矩阵区域称之为“静态脏矩阵”

Arm-2D所提供的场景模板(scene template)中,静态脏矩阵也是以静态列表的形式定义的,比如:

...
/*============================ LOCAL VARIABLES ===============================*/

/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)

    /* a dirty region for hour */
    ADD_REGION_TO_LIST(s_tDirtyRegions,
        ...
    ),

    ...

    /* a dirty region for TenMs */
    ADD_REGION_TO_LIST(s_tDirtyRegions,
        ...
    ),

    /* add the last region for ECG */
    ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
        ...
    ),

END_IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions)

/*============================ IMPLEMENTATION ================================*/
...

其语法格式如下:


/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(<脏矩阵列表名称>, static)

    /* 一个编译时刻位置和尺寸就已经固定下来的脏矩阵矩形区域 */
    ADD_REGION_TO_LIST(<脏矩阵列表名称>,
        .tLocation = {
            .iX = <X轴坐标>,
            .iY = <Y轴坐标>
        },
        .tSize = {
            .iWidth = <宽度>,
            .iHeight = <高度>
        },
    ),

    ...

    /* 一个需要在运行时刻才能确定其位置或大小的脏矩阵矩形区域 */
    ADD_REGION_TO_LIST(<脏矩阵列表名称>,
        0
    ),

    /* 列表的最后一个元素 */
    ADD_LAST_REGION_TO_LIST(<脏矩阵列表名称>,
        ...
    ),


END_IMPL_ARM_2D_REGION_LIST(<脏矩阵列表名称>)

这里,关键字IMPL\_ARM\_2D\_REGION\_LIST() 和配套的 END\_IMPL\_ARM\_2D\_REGION\_LIST() 定义了一个用户指定名称的脏矩阵列表。在它们包裹的区域内,我们只能通过关键字ADD\_REGION\_TO\_LIST()ADD\_LAST\_REGION\_TO\_LIST()来添加具体的矩形区域到列表中。

其中,列表的最后一个元素由ADD\_LAST\_REGION\_TO\_LIST() 来添加,而其它元素则需要通过ADD\_REGION\_TO\_LIST()来添加。值得强调的是:当列表中有且仅有一个元素的时候,也应该使用ADD\_LAST\_REGION\_TO\_LIST() 来添加。

脏矩阵列表中元素的数据类型是 arm\_2d\_region\_list\_item\_t,其定义如下:

/*!
 * \brief the node of a region list
 * 
 */
typedef struct arm_2d_region_list_item_t {
    struct arm_2d_region_list_item_t   *ptNext;                                 //!< the next node
ARM_PRIVATE(
    struct arm_2d_region_list_item_t   *ptInternalNext;                         //!< the next node in the internal list
)
    arm_2d_region_t                     tRegion;                                //!< the region

ARM_PROTECTED(
    uint8_t     chUserRegionIndex;                                              //!< User Region Index, used to indicate updating which dynamic dirty regions  
    uint8_t     bIgnore             : 1;                                        //!< ignore this region
    uint8_t     bUpdated            : 1;                                        //!< this region item has been updated, PFB helper should refresh it again.
    uint8_t                         : 6;                                        //!< reserved for the future

    uint16_t    bFromInternalPool   : 1;                                        //!< a flag indicating whether this list item coming from the internal pool
    uint16_t    bFromHeap           : 1;                                        //!< whether this item comes from the HEAP
    uint16_t    u2UpdateState       : 2;                                        //!< reserved for internal FSM
    uint16_t    u12KEY              : 12;                                       //!< KEY
)

} arm_2d_region_list_item_t;

这里,关键字 ARM\_PRIVATE()ARM\_PROTECTED() 对应C++中的 privateprotected,用以在C语言中实现类成员变量的保护——受此影响,我们能直接访问和设置的就只有“链表指针ptNext”和“描述目标区域的成员tRegion”。

tRegion类型arm\_2d\_region\_t的定义如下:

/*!
 * \brief a type for coordinates (integer)
 *
 */
typedef struct arm_2d_location_t {
    int16_t iX;                         //!< x in Cartesian coordinate system
    int16_t iY;                         //!< y in Cartesian coordinate system
} arm_2d_location_t;

/*!
 * \brief a type for the size of an rectangular area
 *
 */
typedef struct arm_2d_size_t {
    int16_t iWidth;                     //!< width of an rectangular area
    int16_t iHeight;                    //!< height of an rectangular area
} arm_2d_size_t;

/*!
 * \brief a type for an rectangular area
 *
 */
typedef struct arm_2d_region_t {
    implement_ex(arm_2d_location_t, tLocation); //!< the location (top-left corner)
    implement_ex(arm_2d_size_t, tSize);         //!< the size
} arm_2d_region_t;

可见,arm\_2d\_region\_t 通过关键字implement\_ex()同时继承了 arm\_2d\_size\_tarm\_2d\_location\_t。如果你对这里的“继承”感到困惑,其简化后的等效结构体定义如下:

typedef struct arm_2d_region_t {
    struct arm_2d_location_t {
        int16_t iX;                         //!< x in Cartesian coordinate system
        int16_t iY;                         //!< y in Cartesian coordinate system
    } tLocation;
    struct arm_2d_size_t {
        int16_t iWidth;                     //!< width of an rectangular area
        int16_t iHeight;                    //!< height of an rectangular area
    } tSize;
} arm_2d_region_t;

至此,我们能够清晰的理解到,本质上前面所提到的关键字IMPL\_ARM\_2D\_REGION\_LIST()、 END\_IMPL\_ARM\_2D\_REGION\_LIST()、ADD\_REGION\_TO\_LIST() 和 ADD\_LAST\_REGION\_TO\_LIST() 实际上只是通过定义和初始化静态结构体的方法在编译时刻创建了一个静态的链表。

这里,ADD\_REGION\_TO\_LIST() 设置ptNext指针指向下一个元素,而ADD\_LAST\_REGION\_TO\_LIST() 则会将 ptNext设置为NULL——以指示链表的末尾。

除此差别之外,ADD\_REGION\_TO\_LIST()ADD\_LAST\_REGION\_TO\_LIST() 就再无半点不同——实现的都是对结构体成员变量 tRegion 的初始化——方便我们指定目标区域,其语法细节如下图所示:

/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(<脏矩阵列表名称>, static)

    /* 一个编译时刻位置和尺寸就已经固定下来的脏矩阵矩形区域 */
    ADD_REGION_TO_LIST(<脏矩阵列表名称>,
        .tLocation = {
            .iX = <X轴坐标>,
            .iY = <Y轴坐标>
        },
        .tSize = {
            .iWidth = <宽度>,
            .iHeight = <高度>
        },
    ),

    ...
    /* 列表的最后一个元素 */
    ADD_LAST_REGION_TO_LIST(<脏矩阵列表名称>,
        ...
    ),

END_IMPL_ARM_2D_REGION_LIST(<脏矩阵列表名称>)

小结来说:在当前场景中,如果你在编译时刻就已经明确知道“某块脏矩阵的具体坐标和大小”,那么只要借助上述语法对其进行描述即可

至此,你也许会好奇——我们所定义的静态脏矩阵列表是如何与某一个场景(scene)发生关联的呢?

其实,在每一个场景的初始化函数中,我们都会将自己所定义的静态脏矩阵列表直接赋值给 arm\_2d\_scene\_tptDirtyRegion 成员,从而完成列表和场景的绑定,例如:



/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)

    /* a dirty region for hour */
    ADD_REGION_TO_LIST(s_tDirtyRegions,
        ...
    ),

    ...

    /* add the last region for ECG */
    ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
        ...
    ),

END_IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions)

...

/* 场景初始化函数 */
ARM_NONNULL(1)
user_scene_alarm_clock_t *__arm_2d_scene_alarm_clock_init(
                                        arm_2d_scene_player_t *ptDispAdapter, 
                                        user_scene_alarm_clock_t *ptThis)
{
    bool bUserAllocated = false;
    assert(NULL != ptDispAdapter);

    s_tDirtyRegions[dimof(s_tDirtyRegions)-1].ptNext = NULL;


    ...


    *ptThis = (user_scene_alarm_clock_t){
        .use_as__arm_2d_scene_t = {

            ...
            
            /* 将我们定义的静态脏矩阵列表赋值给目标场景 */
            .ptDirtyRegion  = (arm_2d_region_list_item_t *)s_tDirtyRegions,

            ...
        },
        .bUserAllocated = bUserAllocated,
    };

    ...
    
    return ptThis;
}

【“静中有动”】


在前面的文章《【例说Arm-2D界面设计】还在手算坐标?试试Layout Assistant吧!》中,我们其实已经明确了一个想法,即:用户手工计算图像元素的坐标不仅耗时费力,且缺乏对不同分辨率屏幕的自适应能力,代码的可读性也并不如大家想象中的那般友好;与之相反,使用诸如“对齐(Align)”、“停靠(Dock)”、“流式布局(stream layout)” 这类行业惯用的布局手段不仅可以:自动生成位置信息,还可以提升代码可读性、提升图形界面对不同分辨率屏幕的适应性。

就以前面的例子场景(arm\_2d\_scene\_alarm\_clock)为例,其布局示意图如下所示:

image.png

对应的布局代码如下:

static
IMPL_PFB_ON_DRAW(__pfb_draw_scene_alarm_clock_handler)
{
  ...

  arm_2d_canvas(ptTile, __top_canvas) {
  /*-----------------------draw the foreground begin-----------------------*/
        
    /* following code is just a demo, you can remove them */

    arm_2d_dock_vertical(__top_canvas, 64+c_tileECGMask.tRegion.tSize.iHeight) {

      arm_2d_layout(__vertical_region) {

        /* Draw Clock */
        __item_line_dock_vertical(64) {
          arm_2d_size_t tStringSize = arm_lcd_get_string_line_box("00:00:00", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
          arm_2d_size_t tTwoDigitsSizeSmall = arm_lcd_get_string_line_box("00", &ARM_2D_FONT_ALARM_CLOCK_32_A4);
          arm_2d_size_t tTwoDigitsSizeBig = arm_lcd_get_string_line_box("00", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
          arm_2d_size_t tCommaSizeBig = arm_lcd_get_string_line_box(":", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
          arm_2d_size_t tCommaSizeSmall = arm_lcd_get_string_line_box(":", &ARM_2D_FONT_ALARM_CLOCK_32_A4);

          tStringSize.iWidth += tTwoDigitsSizeSmall.iWidth + tCommaSizeSmall.iWidth;

          arm_lcd_text_set_font((arm_2d_font_t *)&ARM_2D_FONT_ALARM_CLOCK_64_A4);
          arm_lcd_text_set_colour(GLCD_COLOR_WHITE, GLCD_COLOR_BLACK);

          arm_2d_dock_horizontal(__item_region, tStringSize.iWidth) {

            arm_2d_layout(__horizontal_region) {

              __item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
                /* 绘制小时数 */
                ...
              }
  
              __item_line_dock_horizontal(tCommaSizeBig.iWidth) {
                /* 绘制冒号 */
                ...
              }
  
              __item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
                /* 绘制分钟数 */
                ...
              }
  
              __item_line_dock_horizontal(tCommaSizeBig.iWidth) {
                /* 绘制冒号 */
                ...
              }
  
              __item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
                 /* 绘制秒数 */
                 ...
              }
  
              __item_line_dock_horizontal(tCommaSizeSmall.iWidth) {
              }
  
              __item_line_dock_horizontal(tTwoDigitsSizeSmall.iWidth) {
                arm_2d_align_centre(__item_region, tTwoDigitsSizeSmall) {
                                      /* 绘制10毫秒数 */
                      ...
                }
              }
            }
          }
        }

        /* ECG Scanning Animation */
        __item_line_dock_vertical() {
          arm_2d_align_centre(__item_region, c_tileECGMask.tRegion.tSize) {
            arm_2d_container(ptTile, __ecg, &__centre_region ) {
                  /* Draw ECG waveform as background */
                            ...
            }
          }
        }
      }
    }
        ...

    /*-----------------------draw the foreground end  -----------------------*/
  }
  arm_2d_op_wait_async(NULL);

  return arm_fsm_rt_cpl;
}

同样的代码在 480*480 的分辨率下,依然可以很好的呈现出所需的视觉效果:

image.png

既然画面本身的绘制是借助布局辅助(Layout Assistant)来完成的,那么我们就无法在编译时刻知晓目标脏矩阵的坐标信息,自然也就无法直接以常量的形式借助ADD\_REGION\_TO\_LIST()ADD\_LAST\_REGION\_TO\_LIST()来直接指定脏矩阵。

怎么办呢?

其实方法很简单,我们可以先使用ADD\_REGION\_TO\_LIST()ADD\_LAST\_REGION\_TO\_LIST() 在列表中“占个座”,然后在场景的初始化函数中使用相同的布局辅助(Layout Assistant)代码来计算出目标脏矩阵所在的位置和大小信息。就前面的例子来说,对应的代码如下:

第一步

先占座

enum {
    DIRTY_REGION_IDX_HOUR,
    DIRTY_REGION_IDX_MIN,
    DIRTY_REGION_IDX_SEC,
    DIRTY_REGION_IDX_TENMS,
    DIRTY_REGION_IDX_ECG,
};

/*! define dirty regions */
IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static)

    /* a dirty region for hour */
    ADD_REGION_TO_LIST(s_tDirtyRegions,
        0  /* initialize at runtime later */
    ),

    /* a dirty region for mins */
    ADD_REGION_TO_LIST(s_tDirtyRegions,
        0  /* initialize at runtime later */
    ),

    /* a dirty region for second */
    ADD_REGION_TO_LIST(s_tDirtyRegions,
        0  /* initialize at runtime later */
    ),

    /* a dirty region for TenMs */
    ADD_REGION_TO_LIST(s_tDirtyRegions,
        0  /* initialize at runtime later */
    ),

    /* add the last region for ECG */
    ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,
        0
    ),

END_IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions)

第二步

在场景初始化代码中用布局辅助(Layout Assistant)计算出目标脏矩阵的区域,并完成对此前占位符的初始化:

ARM_NONNULL(1)
user_scene_alarm_clock_t *__arm_2d_scene_alarm_clock_init(
                                        arm_2d_scene_player_t *ptDispAdapter, 
                                        user_scene_alarm_clock_t *ptThis)
{
    bool bUserAllocated = false;
    assert(NULL != ptDispAdapter);

    s_tDirtyRegions[dimof(s_tDirtyRegions)-1].ptNext = NULL;

    /* get the screen region */
    arm_2d_region_t tScreen
        = arm_2d_helper_pfb_get_display_area(
            &ptDispAdapter->use_as__arm_2d_helper_pfb_t);

    /*--------------initialize static dirty region items: begin---------------*/

    arm_2d_dock_vertical(tScreen, 64+c_tileECGMask.tRegion.tSize.iHeight) {

        arm_2d_layout(__vertical_region) {

            __item_line_dock_vertical(64) {

                arm_2d_size_t tStringSize = arm_lcd_get_string_line_box("00:00:00", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
                arm_2d_size_t tTwoDigitsSizeSmall = arm_lcd_get_string_line_box("00", &ARM_2D_FONT_ALARM_CLOCK_32_A4);
                arm_2d_size_t tTwoDigitsSizeBig = arm_lcd_get_string_line_box("00", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
                arm_2d_size_t tCommaSizeBig = arm_lcd_get_string_line_box(":", &ARM_2D_FONT_ALARM_CLOCK_64_A4);
                arm_2d_size_t tCommaSizeSmall = arm_lcd_get_string_line_box(":", &ARM_2D_FONT_ALARM_CLOCK_32_A4);

                tStringSize.iWidth += tTwoDigitsSizeSmall.iWidth + tCommaSizeSmall.iWidth;

                arm_lcd_text_set_font((arm_2d_font_t *)&ARM_2D_FONT_ALARM_CLOCK_64_A4);
                arm_lcd_text_set_colour(GLCD_COLOR_WHITE, GLCD_COLOR_BLACK);

                arm_2d_dock_horizontal(__item_region, tStringSize.iWidth) {

                    arm_2d_layout(__horizontal_region) {

                        __item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
                            
                            s_tDirtyRegions[DIRTY_REGION_IDX_HOUR].tRegion = __item_region;

                        }

                        __item_line_dock_horizontal(tCommaSizeBig.iWidth) {

                        }

                        __item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
                            s_tDirtyRegions[DIRTY_REGION_IDX_MIN].tRegion = __item_region;
                        }

                        __item_line_dock_horizontal(tCommaSizeBig.iWidth) {
                            
                        }

                        __item_line_dock_horizontal(tTwoDigitsSizeBig.iWidth) {
                            s_tDirtyRegions[DIRTY_REGION_IDX_SEC].tRegion = __item_region;
                        }

                        __item_line_dock_horizontal(tCommaSizeSmall.iWidth) {
                        }

                        __item_line_dock_horizontal(tTwoDigitsSizeSmall.iWidth) {

                            arm_2d_align_centre(__item_region, tTwoDigitsSizeSmall) {
                                s_tDirtyRegions[DIRTY_REGION_IDX_TENMS].tRegion = __centre_region;
                            }
                        }
                    }
                }
            }

            /* ECG Scanning Animation */
            __item_line_dock_vertical() {
                arm_2d_align_centre(__item_region, c_tileECGMask.tRegion.tSize) {
                    s_tDirtyRegions[DIRTY_REGION_IDX_ECG].tRegion = __centre_region;
                }
            }
        }
    }
    /*--------------initialize static dirty region items: end  ---------------*/
    ...

    *ptThis = (user_scene_alarm_clock_t){
        .use_as__arm_2d_scene_t = {
        
             ...
             
            .ptDirtyRegion  = (arm_2d_region_list_item_t *)s_tDirtyRegions,
            
             ...
        },
        .bUserAllocated = bUserAllocated,
    };

    ...

    return ptThis;
}

这里有个细节值得注意:在定义“占位符”的时候,我们还通过枚举的方法在占位符的下标和具体脏矩阵之间建立了增加可读性的关联:

enum {
    DIRTY_REGION_IDX_HOUR,
    DIRTY_REGION_IDX_MIN,
    DIRTY_REGION_IDX_SEC,
    DIRTY_REGION_IDX_TENMS,
    DIRTY_REGION_IDX_ECG,
};

这样,我们后续通过枚举来访问保存在 s\_tDirtyRegions[] 中的具体脏矩阵时,就不会搞错啦,比如:

...
  /* ECG Scanning Animation */
  __item_line_dock_vertical() {
    arm_2d_align_centre(__item_region, c_tileECGMask.tRegion.tSize) {
      s_tDirtyRegions[DIRTY_REGION_IDX_ECG].tRegion = __centre_region;
    }
  }
...

相对在编译时刻(compile-time)以常量赋值的方法直接指定静态脏矩阵的位置和大小;这种在运行时刻(runtime)以布局辅助的方式计算出脏矩阵区域并初始化静态脏矩阵的做法就不可谓不是“静中有动”了。

其实,这里的“动中有静”还有另外一种体现方式。在前面介绍脏矩阵基本概念的时候我们提出过脏矩阵设置的要点,总结来说就是一句话:在“什么时候”刷新“哪个区域”。对静态脏矩阵来说,更新“哪个区域”的问题我们已经解决,剩下就是决定“什么时候”了。

观察应用场景容易发现,很多时候,我们并不需要一直不停地重复刷新某一区域。就以时间显示为例,秒表数字只会一秒钟更新一次、分钟数字一分钟更新一次而表示小时的数字则更是一小时更新一次了。这种情况下,我们只需要在数字变化的瞬间更新一次即可。

为了实现这一功能,Arm-2D 为每个脏矩阵对象都提供了一个API函数:

/*!
 * \brief decide whether ignore the specified dirty region item
 * 
 * \param[in] ptThis the target dirty region item object
 * \param[in] bIgnore whether ignore 
 * \return bool the previous ignore status
 */
extern
ARM_NONNULL(1)
bool arm_2d_dirty_region_item_ignore_set(arm_2d_region_list_item_t *ptThis, bool bIgnore);

用以在无需更新的情况下让 Display Adapter 跳过对目标脏矩阵的刷新。为了避免从原理上就导致画面撕裂,我们应该在场景的 on frame start或者on frame cpl 事件处理程序中调用该函数。还是以时间显示为例,其对应的代码如下:


static void __on_scene_alarm_clock_frame_start(arm_2d_scene_t *ptScene)
{
    user_scene_alarm_clock_t *ptThis = (user_scene_alarm_clock_t *)ptScene;
    ARM_2D_UNUSED(ptThis);

    int64_t lTimeStampInMs = arm_2d_helper_convert_ticks_to_ms(
                                arm_2d_helper_get_system_timestamp());

    /* calculate the hours */
    do {
        uint_fast8_t chHour = lTimeStampInMs / (3600ul * 1000ul);
        chHour %= 24;

        arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_HOUR],
                                            (chHour == this.chHour)); 
        this.chHour = chHour;

        lTimeStampInMs %= (3600ul * 1000ul);
    } while(0);

    /* calculate the Minutes */
    do {
        uint_fast8_t chMin = lTimeStampInMs / (60ul * 1000ul);

        arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_MIN],
                                            (chMin == this.chMin)); 
        this.chMin = chMin;

        lTimeStampInMs %= (60ul * 1000ul);
    } while(0);

    /* calculate the Seconds */
    do {
        uint_fast8_t chSec = lTimeStampInMs / (1000ul);

        arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_SEC],
                                            (chSec == this.chSec)); 
        this.chSec = chSec;

        lTimeStampInMs %= (1000ul);
    } while(0);

    /* calculate the Ten-Miliseconds */
    do {
        uint_fast8_t chTenMs = lTimeStampInMs / (10ul);

        arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_TENMS],
                                            (chTenMs == this.chTenMs)); 
        this.chTenMs = chTenMs;
    } while(0);

    ...
}

这段代码在一开始就获得了当前以毫秒为单位的系统时间:

int64_t lTimeStampInMs 
    = arm_2d_helper_convert_ticks_to_ms(
       arm_2d_helper_get_system_timestamp());

并在后续的代码中依次将其换算为小时、分钟、秒和10毫秒的数字。为了理解如何正确的避免不必要的刷新,我们不妨来具体来看一下秒的处理代码:

 /* calculate the Seconds */
    do {
        uint_fast8_t chSec = lTimeStampInMs / (1000ul);

        arm_2d_dirty_region_item_ignore_set(&s_tDirtyRegions[DIRTY_REGION_IDX_SEC],
                                            (chSec == this.chSec)); 
        this.chSec = chSec;

        lTimeStampInMs %= (1000ul);
    } while(0);

这段代码首先计算出当前的秒数,保存在局部变量 chSec 中;然后调用 函数 arm\_2d\_dirty\_region\_item\_ignore\_set() 来设置“是否要忽略对应脏矩阵的刷新”。这里,借助枚举DIRTY\_REGION\_IDX\_SEC,我们可以轻松的在s\_tDirtyRegions[] 中定位到目标脏矩阵对象,而通过将 chSec 与上一次读秒数字(this.chSec)的对比,我们可以直接将逻辑表达式的计算结果传递给函数作为实参,从而避免了不必要的 if 结构。

为了观察结果,我们可以在 arm\_2d\_disp\_adapter\_0.h 中打开脏矩阵调试模式:

image.png

其对应的宏是:

// <q> Enable Dirty Region Debug Mode
// <i> Draw dirty regions on the screen for debug.
#ifndef __DISP0_CFG_DEBUG_DIRTY_REGIONS__
#   define __DISP0_CFG_DEBUG_DIRTY_REGIONS__  1
#endif

打开脏矩阵调试模式后,Display Adatper 会将当前帧实际刷新的区域用红框标注出来,以方便我们的观察。


值得特别注意的是:在脏矩阵调试模式下,除了标注脏矩阵的范围外,Display Adapter 会实际进行全屏幕刷新——这是为了正确擦除上一帧脏矩阵的红框标记——避免残影干扰观察所必须得。


最终效果如下:

640.gif

可以看到,读秒的区域只会在数字变化的那一帧产生一个红色的方框——标明对应的区域进行了刷新;而10毫秒对应的区域则长期被红色方框标出——这是因为在当前帧率下,该区域经常性的处于变化状态,因此几乎一直处于刷新状态(其实很有可能存在一些帧数字并未发生变化,但我们肉眼就看不到了)。同理,由于底部的波形部分一直处于刷新状态,因此红框也牢牢的锁定了这一区域。

如果你对静态脏矩阵的具体实现范例感兴趣,既可以通过下面的网址直接访问例子代码:

https://github.com/ARM-softwa...\_clock/arm\_2d\_scene\_alarm\_clock.c

也可以在MDK的工程管理器中,选择任意用户创建的Group后单击右键,在弹出菜单中选择 “Add New Item to Group”:

image.png

在窗口左侧的列表中选择最下方的“User Code Template”,然后在右边的窗口中展开 Acceleration,找到 Arm-2D Helper PFB 中的 Scene Template Alarm Clock。单击OK后就可以添加到工程中了。

image.png

感兴趣的朋友可以自行尝试,这里就不再赘述啦。

【追踪动态变化区域的动态脏矩阵】


与静态脏矩阵相对的就是动态脏矩阵(Dynamic Dirty Region)了。动态脏矩阵一般用来追踪画面中位置和大小不断变化的对象。典型的诸如:在屏幕中来回弹跳的小球、变化中的进度条、8BIT游戏中运动的精灵等等。

与静态脏矩阵不同,动态脏矩阵所要覆盖的区域不仅位置是变化的,其尺寸也可能存在差异。这两点综合起来,导致动态脏矩阵不仅仅要考虑当前帧,还要把前一帧中对象所在位置也考虑进来。

比如,在下面这个例子中,小球从前一帧的位置(对应Old Region)移动到了当前帧的位置(对应New Region)。要想正确显示,我们的脏矩阵不仅要覆盖当前帧中小球的位置(好让我们看到它),还要覆盖前一帧中小球所在的位置(好把它过去的残影擦掉)。

image.png

这虽然并不难理解,但却立即带来了至少三种情况,如图所示:

image.png

对于图示的三种情况:

  • 新老位置完全分离:我们需要用两个脏矩阵分别覆盖新老两个位置;
  • 新老位置部分重合:我们需要用一个足以覆盖新老两个位置的最小脏矩阵;
  • 新老位置交错:此时我们要计算足以包含新老位置的“最小覆盖脏矩阵(Minimal Enclosure)”的面积(像素数量),然后与新老两个区域面积的总和进行比较:
  • 如果“最小覆盖脏矩阵”的面积小于新老区域面积总和,则使用最小覆盖脏矩阵进行刷新;反之,
  • 如果“最小覆盖脏矩阵”的面积大于新老区域面积的总和,则维持原来的方案——依次刷新新老两个位置——这里我们不得不接受一定程度的浪费。

到这里,有的朋友会对“新老位置交错”的解决方案产生疑问,难道不是存在一个最优解么?即:把其中一个脏矩阵拆成两份,从而产生一个较大的脏矩阵和两个较小的脏矩阵。

image.png

的确是这样,但从成本考虑,上述拆分方法会产生以下的一些开销:

  • 新增一个脏矩阵带来的RAM开销
  • 选择拆方案的决策开销
  • PFB模式下过于细碎的拆分导致“重复执行用户绘图函数导致的时间成本积累”,抵消了(部分)脏矩阵带来的性能优势
  • 产生更多细碎的脏矩阵,从而更加依赖脏矩阵合并算法。但脏矩阵合并算法本身也存在时间成本随着候选脏矩阵数量增加而快速暴涨的问题。

综合来看,上述成本问题在画面较为简单时还不突出,当画面中要处理的区域增多时,其带来的“时间成本(CPU性能消耗)”和“空间成本(RAM开销)”都会显著增加。

因此选择不对已有的脏矩阵进行拆分——而忍受一定程度浪费的存在——实属权衡利弊后的无奈之选。


实际上,前面讨论的还是“尺寸不变”的前提下带来的简化情况,具体应用中,面对位置和尺寸皆可变化的情况,要考虑的情形往往更为复杂。

好消息是:用户完全不必考虑这些细节,因为Arm-2D提供了一个傻瓜化的辅助服务(Helper Service),实际操作中,只需要无脑的给这个服务提供所要追踪的目标对象在当前帧中的位置即可。翻译成大白话就是:对前面小球的例子来说,我们只需要告诉这个脏矩阵辅助服务小球在当前帧中的位置即可——其它通通不用管啦。

【动态脏矩阵辅助服务模块(arm\_2d\_helper\_dirty\_region)】


动态脏矩阵辅助服务模块由以下基本部分组成:

  • 类(控制块):arm\_2d\_helper\_dirty\_region\_t
  • 目标区域更新函数:arm\_2d\_helper\_dirty\_region\_update\_item()

API的基本构成可以看出,该服务的使用极其简单——直接更新目标区域即可。

下面我们就以显示震动中的氦(Helium)原子核为例,介绍上述API的使用。

640 (1).gif

上图展示的是我们要实现的最终效果,其对应的绘制函数并不复杂——简单来说就是借助 fill-colour-with-mask-and-opacity() 函数分别绘制两个中子、两个质子,通过不同的透明度和适度的错位产生立体的错觉。真正的难度来源于如何产生核帧的效果。这里,我们通过在x轴和y轴上分别叠加两个周期不同、相位不同的简谐振动,实现所需的二维核震错觉:

static void __on_scene_atom_frame_start(arm_2d_scene_t *ptScene)
{
    user_scene_atom_t *ptThis = (user_scene_atom_t *)ptScene;
    ARM_2D_UNUSED(ptThis);

    /* update core and electronics coordinates */
    do {

        int32_t nResult;
        const int16_t iRadiusX = 110;
        const int16_t iRadiusY = 110;

        /* calculate core vibration */
        arm_2d_helper_time_cos_slider(
          -5, 5,                 /* 振动幅度 */
          100,                   /* 振动周期 */
          ARM_2D_ANGLE(0.0f),    /* 相位 */
          &nResult, &this.lTimestamp[1]);
        this.Core.tVibration.iX = nResult;
        
        arm_2d_helper_time_cos_slider(
          -5, 5,                 /* 振动幅度 */
          150,                   /* 振动周期 */
          ARM_2D_ANGLE(30.0f),   /* 相位 */
          &nResult, &this.lTimestamp[2]);
        this.Core.tVibration.iY = nResult;
        
        ...

    } while(0);

    ...
}

好了,舞台已经准备就绪,演员就位,下面就是脏矩阵的表演时刻了。

第一步:

在场景中初始化中打开 arm\_2d\_scene\_t** 自带的脏矩阵服务模块:

ARM_NONNULL(1)
user_scene_atom_t *__arm_2d_scene_atom_init(   arm_2d_scene_player_t *ptDispAdapter, 
                                        user_scene_atom_t *ptThis)
{

    ...

    *ptThis = (user_scene_atom_t){
        .use_as__arm_2d_scene_t = {
              
            ...

            .bUseDirtyRegionHelper = true,
        },

        ...
    };

    ...
    return ptThis;
}

第二步:

在场景绘图函数中更新目标区域。

/* update dirty region */
  arm_2d_helper_dirty_region_update_item(
      &this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
      &this.use_as__arm_2d_scene_t.tDirtyRegionHelper.tDefaultItem,
      
      (arm_2d_tile_t *)< 目标 tile 指针 >,
      < 脏矩阵目标区域所在的画布的地址 >,
      < 脏矩阵目标区域的地址 >);
    
  

其中:

  • <目标 tile 指针> 指向当前绘图的目标tile
  • < 脏矩阵目标区域所在的画布的地址 > :当我们提供画布的地址后,如果脏矩阵超出了该画布就会被自动裁剪(甚至忽略掉)。该参数可以为NULL
  • < 脏矩阵目标区域的地址 > : 这就是当前帧内我们要追踪的目标所在的区域信息。

在这个氦原子核振动的例子中,我们可以使用下面的实现脏矩阵对原子核的追踪:

static
IMPL_PFB_ON_DRAW(__pfb_draw_scene_atom_handler)
{
  ARM_2D_PARAM(pTarget);
  ARM_2D_PARAM(ptTile);
  ARM_2D_PARAM(bIsNewFrame);
  
  user_scene_atom_t *ptThis = (user_scene_atom_t *)pTarget;
  arm_2d_size_t tScreenSize = ptTile->tRegion.tSize;
  
  ARM_2D_UNUSED(tScreenSize);
  
  arm_2d_canvas(ptTile, __top_canvas) {
  /*-----------------------draw the foreground begin-----------------------*/
  
    arm_2d_size_t tCharSize 
      = ARM_2D_FONT_A4_DIGITS_ONLY
          .use_as__arm_2d_user_font_t
              .use_as__arm_2d_font_t.tCharSize;
    arm_2d_size_t tAtomCoreSize = {
      .iWidth  = c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iWidth * 2,
      .iHeight = c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iHeight * 2 
               + tCharSize.iHeight,
    };
    
    /* draw atom core */
    arm_2d_align_centre(__top_canvas, tAtomCoreSize) {

      arm_2d_layout(__centre_region) {
      
        __item_line_dock_vertical(c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iHeight * 2) {
            
          /* apply vibration */
          __item_region.tLocation.iX += this.Core.tVibration.iX;
          __item_region.tLocation.iY += this.Core.tVibration.iY;

          arm_2d_region_t tDirtyRegion = __item_region;
          tDirtyRegion.tSize.iHeight += tCharSize.iHeight;

          /* update dirty region */
          arm_2d_helper_dirty_region_update_item(
            &this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
            &this.use_as__arm_2d_scene_t.tDirtyRegionHelper.tDefaultItem,
            (arm_2d_tile_t *)ptTile,
            &__top_canvas,
            &tDirtyRegion);

            ...
          
        }

        ...
      }
    }

    ...

  /*-----------------------draw the foreground end  -----------------------*/
  }
  arm_2d_op_wait_async(NULL);
  
  return arm_fsm_rt_cpl;
}

至此,大功告成。是不是非常简单呢?

【当你要更新多个运动对象时该怎么办?】


在前面的例子中,我们通过两个步骤就实现了使用脏矩阵对单个移动目标的追踪。假设一个场景中存在多个移动目标,阁下又当如何应对呢?

不慌,其实脏矩阵服务模块本身就支持多目标的追踪——可以通过向其追加 arm\_2d\_helper\_dirty\_region\_item\_t 对象的形式,配合 arm\_2d\_helper\_dirty\_region\_update\_item() 函数来完成对复数目标的覆盖。我们以绘制氦原子的卢瑟福模型为例,在前面原子核的基础上加上两个电子,其最终效果如下所示:

640 (2).gif

这里,电子在二维平面上的运动轨迹仍然是通过x轴和y轴上的简谐振动叠加而成

static void __on_scene_atom_frame_start(arm_2d_scene_t *ptScene)
{
  user_scene_atom_t *ptThis = (user_scene_atom_t *)ptScene;
  ARM_2D_UNUSED(ptThis);

  /* update core and electronics coordinates */
  do {

    int32_t nResult;
    const int16_t iRadiusX = 110;
    const int16_t iRadiusY = 110;

    ...

    /* calculate electronic0 vibration */
    arm_2d_helper_time_cos_slider(
      -iRadiusX, iRadiusX, /* 振动幅度 */
      1300,                /* 周期 */
      ARM_2D_ANGLE(0.0f),  /* 相位 */
      &nResult, &this.lTimestamp[3]);
    this.Electronic[0].tOffset.iX = nResult;
    
    arm_2d_helper_time_cos_slider(
      -iRadiusY, iRadiusY, /* 振动幅度 */
      1300,                /* 周期 */
      ARM_2D_ANGLE(45.0f), /* 相位 */
      &nResult, &this.lTimestamp[4]);
    this.Electronic[0].tOffset.iY = nResult;

    arm_2d_helper_time_cos_slider(
      128, 255, 
      1300, 
      ARM_2D_ANGLE(45.0f), 
      &nResult, &this.lTimestamp[7]);
    this.Electronic[0].chOpacity = nResult;
        
    ...

  } while(0);

}

值得说明的是,我们甚至使用相同周期变化的 透明度 模拟了电子在Z轴上的远近变化。完成了电子运动轨迹的生成,接下来就是脏矩阵的覆盖和更新了。

在前一章节步骤的基础上,追加电子的操作如下:

第一步:

在用户的场景对象定义中添加 arm\_2d\_helper\_dirty\_region\_item\_t 成员。

/*!
 * \brief a user class for scene atom
 */
typedef struct user_scene_atom_t user_scene_atom_t;

struct user_scene_atom_t {
    implement(arm_2d_scene_t);                                                  //! derived from class: arm_2d_scene_t

ARM_PRIVATE(

    ...

    struct {
        arm_2d_helper_dirty_region_item_t tDirtyRegionItem;
        arm_2d_location_t tOffset;
        uint8_t chOpacity;
    } Electronic[2];

)
    /* place your public member here */
    
};

第二步:

向脏矩阵辅助模块添加 arm\_2d\_helper\_dirty\_region\_item\_t 对象。

首先,我们要确保场景中实现了 on load 事件处理程序,具体可以在场景初始化函数中加入下面的代码:

static void __on_scene_atom_load(arm_2d_scene_t *ptScene)
{
    user_scene_atom_t *ptThis = (user_scene_atom_t *)ptScene;
    ARM_2D_UNUSED(ptThis);

    ...

}


ARM_NONNULL(1)
user_scene_atom_t *__arm_2d_scene_atom_init(   arm_2d_scene_player_t *ptDispAdapter, 
                                        user_scene_atom_t *ptThis)
{
    ...

    *ptThis = (user_scene_atom_t){
        .use_as__arm_2d_scene_t = {

            ...
            
            /* Please uncommon the callbacks if you need them
             */
            .fnOnLoad       = &__on_scene_atom_load,
            
            ...

            .bUseDirtyRegionHelper = true,
        },
        ...
    };

    ...
}

接下来在场景 on load 事件处理程序中添加代码:

static void __on_scene_atom_load(arm_2d_scene_t *ptScene)
{
    user_scene_atom_t *ptThis = (user_scene_atom_t *)ptScene;
    ARM_2D_UNUSED(ptThis);

    arm_2d_helper_dirty_region_add_items(
      &this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
      &this.Electronic[0].tDirtyRegionItem,
      1);

    arm_2d_helper_dirty_region_add_items(
      &this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
      &this.Electronic[1].tDirtyRegionItem,
      1);
}

这里,函数 arm\_2d\_helper\_dirty\_region\_add\_items() 的原型如下:

extern
ARM_NONNULL(1,2)
/*!
 * \brief add an array of region items to a dirty region helper 
 * 
 * \param[in] ptThis the target helper
 * \param[in] ptItems the array of the region items
 * \param[in] hwCount the number of items in the array
 */
void arm_2d_helper_dirty_region_add_items(
        arm_2d_helper_dirty_region_t *ptThis,
        arm_2d_helper_dirty_region_item_t *ptItems,
        uint_fast16_t hwCount)

extern
ARM_NONNULL(1,2)
/*!
 * \brief remove an array of region items to a dirty region helper 
 * 
 * \param[in] ptThis the target helper
 * \param[in] ptItems the array of the region items
 * \param[in] hwCount the number of items in the array
 */
void arm_2d_helper_dirty_region_remove_items(
        arm_2d_helper_dirty_region_t *ptThis,
        arm_2d_helper_dirty_region_item_t *ptItems,
        uint_fast16_t hwCount);

观察函数原型容易发现,通过 ptItemshwCount,我们可以轻松的一次性添加多个对象。在需要的时候,我们也可以通过函数 arm\_2d\_helper\_dirty\_region\_remove\_item() 将此前加入的一些对象移除,这里就不再赘述。

第三步:

在场景绘图函数中更新脏矩阵区域。

其实,arm\_2d\_helper\_dirty\_region\_t 控制块中自带了一个默认的 arm\_2d\_helper\_dirty\_region\_item\_t 对象 tDefaultItem,这也是为什么之前追踪原子核的例子中会使用下面的代码来更新目标区域:

...
arm_2d_helper_dirty_region_update_item(
    &this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
    
    &this
      .use_as__arm_2d_scene_t
        .tDirtyRegionHelper
          .tDefaultItem,
          
    (arm_2d_tile_t *)ptTile,
    &__top_canvas,
    &tDirtyRegion);
...

因此,两个电子对应区域的更新,本质上并无不同:

                        
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene_atom_handler)
{
  ...

  user_scene_atom_t *ptThis = (user_scene_atom_t *)pTarget;
  arm_2d_size_t tScreenSize = ptTile->tRegion.tSize;

  arm_2d_canvas(ptTile, __top_canvas) {
  /*-----------------------draw the foreground begin-----------------------*/

    arm_2d_size_t tCharSize = ARM_2D_FONT_A4_DIGITS_ONLY
                            .use_as__arm_2d_user_font_t
                                .use_as__arm_2d_font_t.tCharSize;
    arm_2d_size_t tAtomCoreSize = {
        .iWidth = c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iWidth * 2,
        .iHeight = c_tileWhiteDotMiddleA4Mask.tRegion.tSize.iHeight * 2 
                 + tCharSize.iHeight,
    };
    
    。。。

    /* draw electronics */
    do {
      arm_2d_align_centre(__top_canvas, 
                          220, 
                          220) {

        arm_2d_size_t tElectronicSize = {
          .iWidth = c_tileWhiteDotMask.tRegion.tSize.iWidth
                  + tCharSize.iWidth,
          .iHeight = c_tileWhiteDotMask.tRegion.tSize.iWidth
                  + tCharSize.iHeight,
        };

        /* electronic 0 */
        arm_2d_align_centre(__centre_region, tElectronicSize) {

          __centre_region.tLocation.iX += this.Electronic[0].tOffset.iX;
          __centre_region.tLocation.iY += this.Electronic[0].tOffset.iY;
    
          /* update dirty region */
          arm_2d_helper_dirty_region_update_item( 
            &this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
            &this.Electronic[0].tDirtyRegionItem,
            (arm_2d_tile_t *)ptTile,
            &__top_canvas,
            &__centre_region);

          ...
        }
        
        /* electronic 1 */
        arm_2d_align_centre(__centre_region, tElectronicSize) {

          __centre_region.tLocation.iX += this.Electronic[1].tOffset.iX;
          __centre_region.tLocation.iY += this.Electronic[1].tOffset.iY;

          /* update dirty region */
          arm_2d_helper_dirty_region_update_item( 
            &this.use_as__arm_2d_scene_t.tDirtyRegionHelper,
            &this.Electronic[1].tDirtyRegionItem,
            (arm_2d_tile_t *)ptTile,
            &__top_canvas,
            &__centre_region);
          ...
        }

      }
    } while(0);

  /*-----------------------draw the foreground end  -----------------------*/
  }
  arm_2d_op_wait_async(NULL);

  return arm_fsm_rt_cpl;
}

动态脏矩阵辅助服务模块的其它操作


为了满足不同的应用需要,动态脏矩阵辅助服务模块还提供了以下的一些API函数:

/*!
 * \brief force the arm_2d_helper_dirty_region_item_t object to suspend the 
 *        dirty region update.
 * 
 * \param[in] ptThis the target item
 * \param[in] bEnable whether enable this feature.
 * \return boolean the original setting
 */
ARM_NONNULL(1)
bool arm_2d_helper_dirty_region_item_suspend_update(
      arm_2d_helper_dirty_region_item_t *ptThis,
      bool bEnable);

该函数与静态脏矩阵的 arm\_2d\_dirty\_region\_item\_ignore\_set() 函数功能类似——都是迫使目标 arm\_2d\_helper\_dirty\_region\_item\_t 所对应的区域放弃更新。

/*!
 * \brief force an arm_2d_helper_dirty_region_item_t object to use the minimal
 *         enclosure region to update.
 * 
 * \param[in] ptThis the target item
 * \param[in] bEnable whether enable this feature.
 * \return boolean the original setting
 */
ARM_NONNULL(1)
bool arm_2d_helper_dirty_region_item_force_to_use_minimal_enclosure(
          arm_2d_helper_dirty_region_item_t *ptThis,
          bool bEnable);

该函数会强迫目标 arm\_2d\_helper\_dirty\_region\_item\_t 对象使用“最小覆盖脏矩阵”来作为刷新区域。这在某些应用场景下非常有用。比如在下面的进度条例子中,假设进度条发生了突变(图中从右向左出现了位移),相比原本只覆盖圆形断点一前一后两个位置,要想正确的更新进度条凹槽内的背景,我们就必须要使用能同时覆盖新老两个区域的“最小覆盖脏矩阵”
image.png

【说在后面的话】


人们常说,图形界面的优化就是欺骗的艺术,而脏矩阵的设计就是这类欺骗技术中最基础的一种。

每当我们面对孱弱的CPU性能、或是透过Virtual Resource从外部SPI Flash中蹒跚的载入号称处理器不可承受之重的资源时,脏矩阵就像是黑暗中的一盏救赎的明灯——把客户骗的一愣一愣

原文:裸机思维
作者:裸机思维

专栏推荐文章

如果你喜欢我的思维,欢迎订阅裸机思维欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1468
内容数
110
探讨嵌入式系统开发的相关思维、方法、技巧。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息