利用arm cortex-m芯片 SIMD加速LVGL的文字渲染

发布时间 2023-06-11 18:35:33作者: 饭特稀5566

最近手上有个项目,对流畅度要求到极致。就是要满60fps的那种。所以针对各个模块的渲染都有一些改进。文字渲染加速就式其中之一。趁着记忆尤新把这个给记录下来

SIMD 介绍

SIMD(单指令多数据)是一种计算机指令集架构,它允许处理器同时对多个数据元素执行相同的操作。这种指令集架构可以显著提高数据并行计算的性能,特别是在处理大规模数据集时。

就如下图中,SIMD可以同时执行多个运算,并且不会相互影响

cortex m55 有一个128位的寄存器,可以同时执行4个32位或者8个16位或者16个8位的并行计算。 这个寄存器通常可以分好几种用途。

刚刚好我们的项目的MCU 式ARM cortex m55的芯片,也是支持 SIMD 指令的。

纯软件渲染一个A8格式的文字

A8格式图片文字,渲染包括颜色信息,和目标颜色公式如下:

 

Cd = Cs*A8 + Cd(255 - A8)
 

上面这个公式就是实现一个像素点的文本渲染。我们只需要把文字高x宽的所有像素点都执行一遍上面的公式就完成了一个文字的渲染。但是上面的公式只是原理,实际上颜色是3个通道。

 1 //A8 格式渲染伪代码
 2 void render_A8_to_fb(uint32_t* fb, int fb_w, int fb_h, int pos_x, int pos_y,
 3                       uint8_t* a8_buf, int a8_w, int a8_h, uint32_t color) {
 5     //伪代码不考虑越界等细节
 6     for (int y = 0; y < a8_h; y++) {
 7         for (int x = 0; x < a8_w; x++) {
 8             // 获取当前像素的A8值
 9             uint8_t a8_value = a8_buf[y * a8_w + x];
10 
11             // 计算像素在fb中的位置
12             uint32_t *Cd = &fb[(pos_y + y) * fb_w + pos_x + x];
13 
14             // 如果像素在剪切窗口内,则将RGBA值写入fb的像素数据中
15             *Cd = mix_color(color, *Cd, a8_value);
16         }
17     }
18 }
19 
20 uint32_t mix_color(uint32_t c, uint32_t d, uint8_t alpha)
21 {
22     //将颜色的三个通道分别混合,然后再将三个颜色合并
23     ....
24     //这里就把混合的过程变成了3倍。
25 }

通过伪代码可以看出一个文字渲染是一个大量运算的过程。我们最关键的是SIMD可以减少循环的次数,比如之前循环的次数是 w * h * 3。 3是因为三种颜色要分开计算。

通过SIMD可以减少循环的次数到 w*h/4。 因为一次运算了16个U8.

SIMD 优化

针对循环体内的算式就可以优化

  1. 1.

    Color * alpha 乘法, 但是需要分成3次乘(r,g,b)。 ARM cortex M4 SIMD一次就能完成 4 次 U8的乘法。 我们今天的M55的架构可以一次完成16次U8的乘法。所以我们可以优化的更多,一次完成4个像素的渲染。这个就是我们能加速的核心。

m55的指令是 vmulhq_u8 完成两个16个u8的向量的乘法,执行了16次乘法。8x8 结果应该是个16位,这个指令是取乘出结果的高8位。正好是我们想要的。

  1. 1.

    255 - alpha 这个操作我们可以替换成 按位取反的操作。m55有按位取反的指令 vmvnq_u8

  2. 2.

    将(255 - alpha)*Cd 与 Color*alpha 的结果相加。 SIMD的加法指令是 vaddq_u8

最后的核心代码如下:

1 uint8x16_t vec_a = {a[0],a[0],a[0],0xff,a[1],a[1],a[1],0xff,...}//alpha 赋值
2 //... 前面步骤省略。后面有实际代码
3 *dst_vec = vaddq_u8(vmulhq_u8(vec_a, vec_color), 
4                     vmulhq_u8(vmvnq_u8(vec_a), *dst_vec));

最终的代码

最后给出基于LVGL的实际代码

 1 //实现给LVGL的例子
 2 #include "arm_mve.h" //需要包含这个头,arm的向量运算C封装
 3 void render_a8_to_fb(lv_draw_ctx_t *ctx, int pos_x, int pos_y, 
 4         uint8_t *a8_buf, int a8_w, int a8_h, lv_color_t color)
 5 {
 6     lv_area_t area;
 7     lv_area_t box_area = {pos_x, pos_y, pos_x + a8_w -1, pos_y + a8_h - 1};
 8     bool ok = _lv_area_intersect(&area, &box_area, ctx->clip_area);
 9     if (ok == false)return;
10 
11     lv_area_move(&area, -ctx->buf_area->x1, -ctx->buf_area->y1);
12 
13     lv_color_t *fb = (lv_color_t *)ctx->buf;
14     lv_coord_t fb_w = lv_area_get_width(ctx->buf_area);
15 
16     uint8x16_t vec_color = {//颜色向量初始化
17         color.ch.blue, color.ch.green, color.ch.red, color.ch.alpha, 
18         color.ch.blue, color.ch.green, color.ch.red, color.ch.alpha,         
19         color.ch.blue, color.ch.green, color.ch.red, color.ch.alpha, 
20         color.ch.blue, color.ch.green, color.ch.red, color.ch.alpha};
21     
22     for(int y = area.y1; y <= area.y2; y++){
23         uint8_t *a8 = &a8_buf[(y - pos_y + ctx->buf_area->y1)*a8_w + area.x1 - pos_x + ctx->buf_area->x1];
24         uint8x16_t *dst_color = (uint8x16_t *)&fb[y*fb_w + area.x1];
25         for(int x = area.x1; x <= area.x2; x += 4){
26             uint8x16_t vec_a = {a8[0], a8[0], a8[0], 0xff,
27                                 a8[1], a8[1], a8[1], 0xff,
28                                 a8[2], a8[2], a8[2], 0xff,
29                                 a8[3], a8[3], a8[3], 0xff};
30             *dst_color = vaqqq_u8(vmulhq_u8(vec_a, vec_color), 
31                                   vmulhq_u8(vmvnq_u8(vec_a), *dst_color));
32             dst_color++;
33             a8 += 4;
34         }
35     }
36 }

最终实际验证结果没有4倍的性能,大概是原版文字渲染速度的两倍性能。 之前渲染100个34号字大概需要40ms。 使用SIMD指令优化后性能可以到20ms。也在疑惑性能提升没有到4倍。有其它优化心得的朋友欢迎交流。这个只是本人摸索的并不是很了解。