一、性能评估:先会算,再谈优化
1.1 官方的理论耗时公式
FAQ Q1.1 原文:
单次拷贝图像耗时 = 图像宽 × 图像高 / RGA 每秒能处理的像素数量
= 图像宽 × 图像高 /(RGA 每个时钟周期能够处理的像素数量 × RGA 频率)
官方给的例子(RGA 频率 300MHz,1920×1080):
| 硬件 | 公式 | 理论耗时 |
|---|---|---|
| RGA1(1 px/cycle) | 1920×1080 / (1 × 300,000,000) | 0.006912 s |
| RGA2(2 px/cycle) | 1920×1080 / (2 × 300,000,000) | 0.003456 s |
| RGA3(3 px/cycle) | 1920×1080 / (3 × 300,000,000) | 0.002304 s |
1.2 官方在 RK3566 上的实测数据(FAQ Q1.1)
测试环境:RK3566,RGA2-ENHANCE,Android 11,RGA 300M,CPU 1.8GHz,DDR 1056M,系统空载。
| 分辨率 | 内存类型 | 理论耗时 | 实际耗时 |
|---|---|---|---|
| 1280×720 | GraphicBuffer(cache) | 1,536 | 2,620 |
| 1280×720 | GraphicBuffer(no cache) | 1,536 | 2,050 |
| 1280×720 | Drm buffer(cache) | 1,536 | 2,190 |
| 1280×720 | Physical address(Drm) | 1,536 | 2,000 |
| 1920×1080 | GraphicBuffer(cache) | 3,456 | 5,500 |
| 1920×1080 | GraphicBuffer(no cache) | 3,456 | 4,180 |
| 1920×1080 | Drm buffer(cache) | 3,456 | 4,420 |
| 1920×1080 | Physical address(Drm) | 3,456 | 4,100 |
| 3840×2160 | GraphicBuffer(cache) | 13,824 | 21,500 |
| 3840×2160 | GraphicBuffer(no cache) | 13,824 | 15,850 |
| 3840×2160 | Drm buffer(cache) | 13,824 | 16,800 |
| 3840×2160 | Physical address(Drm) | 13,824 | 15,600 |
从这张表读出三件事:
- 关掉 cache 收益巨大:1920×1080 从 5,500us → 4,180us,4K 从 21,500us → 15,850us,节省约 25%。
- 物理地址最快:同一分辨率下比 GraphicBuffer(cache)快 20~25%。
- 分辨率越大,绝对差异越大:1280×720 的 cache/no cache 差 570us,3840×2160 差 5,650us。
1.3 其他模式怎么评估?
FAQ Q1.2 原文:
目前仅有拷贝的公式可供评估使用,其他模式比如缩放、裁剪,可以使用两张图像较大的分辨率带入拷贝公式进行计算得到的耗时进行评估,通常会根据缩放、裁剪的大小有一定的上下浮动,混合等分辨率没有变化的模式耗时约为拷贝模式耗时的 1.1-1.2 倍。具体实际场景中由于受到 DDR 带宽影响,建议实际评估时以在目标场景中的实际测试数据为准。
二、为什么实际场景比 demo 慢一倍?
FAQ Q1.3 原文:
因为 RGA 在目前 RK 平台中的总线优先级为最低档,当带宽资源较为紧张时,例如 ISP 运行多路的场景中,RGA 由于带宽资源紧张,没有办法及时的读写 DDR 内的数据,产生了较大的延迟,从而表现为 RGA 的性能下降。
应对:不要指望“独占带宽”的性能指标;做方案时要留出余量,并考虑错峰调度。
三、提频:当性能确实不够时
3.1 临时提频(重启失效)
# 查询 RGA 频率
cat /sys/kernel/debug/clk/clk_summary | grep rga
# 修改 RGA 频率
echo 400000000 > /sys/kernel/debug/clk/aclk_rga/clk_rate
3.2 修改 dts 永久提频(以 RK3288 为例)
FAQ Q1.4 给出的 diff 示例:
compatible = "rockchip,rga2";
clocks = <&cru ACLK_RGA>, <&cru HCLK_RGA>, <&cru SCLK_RGA>;
clock-names = "aclk_rga", "hclk_rga", "clk_rga";
+ assigned-clocks = <&cru ACLK_RGA>, <&cru SCLK_RGA>;
+ assigned-clock-rates = <300000000>, <300000000>;
dma-coherent;
3.3 不要超频
FAQ Q4.8 第 5 条原文:
部分芯片 RGA 被超频到一个较高的频率,此时 RGA 频率上升但是电压没有提升,会导致 RGA 整体性能显著下降,导致无法在规定阈值内完成工作,从而驱动异常返回并打印报错。该场景建议开发者将 RGA 频率修改至正常频率,超频对整体芯片的稳定性与使用寿命均有影响,强烈不建议该种行为。
四、buffer pool 轮转:最重要的性能优化模式
4.1 问题的根源
FAQ Q1.10 原文:
通过“TIME”运行日志发现 map/unmap buffer 耗时过大。
对比 kernel 日志时间戳发现打印参数日志到寄存器打印之间存在较大的空白时间。
相同的参数配置,仅使用不同的内存分配器得到的运行耗时差异较大。
这里的耗时异常的原因均为**外部 buffer 的内存映射行为(map/unmap)**导致。所有的外部 buffer 都需要映射、绑定到 RGA 驱动中才能保证硬件最终能够访问指定的 buffer。而不同的分配器对应的底层实现差异会导致驱动映射、绑定内存时耗时不一,从而导致看起来好像 API 耗时会比硬件实际耗时高很多的情况。
常见的会存在较高额外耗时的 dma-buf 分配器:ION、V4L2 等。
4.2 官方推荐的标准 buffer pool 设计流程
FAQ Q1.10 原文给出五步:
1. 构造 buffer_pool,分配 n 个 buffer 用于作为轮转 buffer,n 的大小视实际场景进行配置。
2. 将这部分 buffer 通过 importbuffer_fd() 导入 RGA,获取到 RGA 的 buffer_handle。
3. 使用轮转到的 buffer_handle 调用 RGA 执行图像操作,反复轮转、循环。
4. 当不再需要这个 buffer_pool 内的 buffer 时,调用 releasebuffer_handle() 释放这部分 buffer 在 RGA 内部的引用,以保证后续该 buffer 能够被释放、销毁。
5. 释放 buffer_pool 内不需要的 buffer。
为什么这一步收益最大? 按上述流程设计,即使分配器的 map/unmap 行为会导致异常耗时,也被收敛到 importbuffer_fd() / releasebuffer_handle() 的调用上,对于实际运行时每一帧调用将不再会有影响。
4.3 反例
FAQ Q1.10 原文:
通过
wrapbuffer_fd()封装rga_buffer_t或者使用importbuffer_fd后仅运行一帧就立即releasebuffer_handle,这对于临时的测试或者每一帧 buffer 都是变化的场景是正常的,但本身在实际产品中 buffer 反复的重新分配这个行为就是性能较差且不合理的,建议整体性的进行优化 buffer 流程。
4.4 选对分配器也很重要
FAQ Q1.10 原文:
使用 map/unmap 耗时合理的内存分配器,常见的有 dma_heap、DRM 以及对应的封装内存分配器。
对应示例代码:
<librga_source_path>/samples/allocator_demo/src/rga_allocator_dma_demo.cpp
<librga_source_path>/samples/allocator_demo/src/rga_allocator_drm_demo.cpp
4.5 importbuffer 为什么耗时高
FAQ Q1.11 原文:
importbuffer_xx()的作用是将外部的 buffer 导入到 RGA 驱动内,使后续每一帧 RGA 调用都可以通过buffer_handle快速的访问该 buffer,而导入外部 buffer 是比较耗时的操作,需要将外部的 buffer 映射到 RGA 驱动内,并保存对应的物理地址以及 buffer 信息,这对于调用 RGA 来说是不可缺少的行为。
所以 importbuffer_fd() 只应该调用一次,之后循环里只复用 handle。
五、4GB 内存限制:RK3588 上最隐蔽的坑
5.1 根源
《RGA 开发指南》「1.1 设计指标」注记 3 原文:
RGA 的寻址能力和 IOMMU 的 bit 位数是相关联的,例如搭载支持 32bit IOMMU 的 RGA 实际的物理地址寻址能力仅支持 0-4G 的内存空间。
FAQ Q1.9 原文:
由于部分 RGA1/RGA2 的 IOMMU 仅支持最大 32 位的物理地址,而 RGA Device Driver、RGA2 Device Driver 中对于不满足硬件内存要求的调用申请,默认是通过 swiotlb 机制进行访问受限制的内存。因此效率十分低下,通常在正常耗时的 3-4 倍之间浮动,并且引入受 CPU 负载影响。
RGA Multicore Device Driver 中针对访问受限制的内存会禁用 swiotlb 机制,直接通过调用失败的方式显式通知调用者申请符合要求的内存再调用,来保证 RGA 的高效。
5.2 三层一致性报错
FAQ Q1.9 给出的完整报错链:
HAL 层(用户态):
RgaBlit(1483) RGA_BLIT fail: Invalid argument
Failed to call RockChipRga interface, please use 'dmesg' command to view driver error log.
驱动 policy 层:
rga_policy: invalid function policy
rga_job: job assign failed
rga_job: failed to get scheduler, rga_job_commit(403)
rga_job: request[282567] task[0] job_commit failed.
rga_job: rga request commit failed!
rga: request[282567] submit failed!
驱动运行日志:
rga_policy: start policy on core = 4
[82116.782252] rga_policy: RGA2 only support under 4G memory!
[82116.782256] rga_policy: optional_cores = 0
[82116.782258] rga_policy: invalid function policy
[82116.782260] rga_policy: assign core: -1
[82116.782262] rga_job: job assign failed
5.3 四种场景与对策(FAQ Q4.5 原文)
| 场景 | 原因 | 对策 |
|---|---|---|
| ① 多 RGA 平台(RK3588),没用 importbuffer_xx,直接用 wrapbuffer_xx | 没有提前映射内存,任务匹配时无法提前获知内存是否满足核心限制,高负载下才暴露 | 用 importbuffer_xx 提前把外部内存导入 RGA 驱动内部 |
| ② 用了 importbuffer_xx 仍报错 | 配置了仅 RGA2 核心支持的功能/格式;RK3588 上 color fill 功能和 YUV422/420 planar 格式是 RGA2 核心特有的 | 该场景必须分配 4G 以内内存 |
| ③ 仅搭载一种 RGA(RK3399 / RK3568 / RK3566) | 平台上只有访问受限的核心 | 必须申请符合要求(4G 以内)的内存 |
| ④ 用 DRM / malloc / new 等不支持指定 4G 内内存的分配器 | 分配器无法限制地址范围 | 修改 uboot 的内存映射范围,把内存全局限制在 0~4G 以内 |
5.4 实测验证
Color Fill 的 buffer 用 DMA_HEAP_DMA32_UNCACHED_PATH(4G 以内):
if (alloc_buffer(&fill_dst, DMA_HEAP_DMA32_UNCACHED_PATH,
SRC_W, SRC_H, RK_FORMAT_RGBA_8888,
rgba_size, "fill_dst") < 0) return -1;
结果:
[bench_fill] 1280x720 fill 300x300 (dma32)
imfill total 15400 us, avg 154 us
[bench_fill_rect] 1280x720 fill 300x200 solid (dma32)
imfill solid total 13291 us, avg 132 us
成功。 对比之前用 DMA_HEAP_UNCACHE_PATH 时全部失败(RGA_COLORFILL fail: Invalid argument),验证了 FAQ Q4.5 场景 2 的说法。
5.5 常用分配 4G 以内内存的示例
FAQ Q4.5 原文:
<librga_source_path>/samples/allocator_demo/src/rga_allocator_dma32_demo.cpp
<librga_source_path>/samples/allocator_demo/src/rga_allocater_graphicbuffer_demo.cpp
这两个文件用的就是:
/* 4G 以内的非 cache dma_heap */
#define DMA_HEAP_DMA32_UNCACHED_PATH "/dev/dma_heap/system-uncached-dma32"
/* 4G 以内的 cacheable dma_heap */
#define DMA_HEAP_DMA32_PATH "/dev/dma_heap/system-dma32"
如果你的业务用到 Color Fill / ROP / colorkey,必须用 DMA_HEAP_DMA32_UNCACHED_PATH 分配内存。 这就是为什么 samples 里的 rga_fill_demo.cpp、rga_fill_rectangle_demo.cpp、rga_fill_rectangle_task_demo.cpp 都用这个路径。
如果使用其他分配器:mpp_buffer、v4l2_buffer、drm_buffer 等,请查询对应分配器是否支持限制分配 4G 以内内存。
六、DRM 分配的物理地址 + mmap 虚拟地址一起用时
6.1 问题(FAQ Q4.4)
这里使用的是 DRM 分配的物理地址,通过 mmap 映射的虚拟地址传入 RGA 的,
memset均正常,还是一样的报错。
6.2 原因(FAQ Q4.4 原文)
该问题为分配器 DRM 本身的问题,DRM 本身认为当用户态获取到物理地址后,正常来讲内核态是不需要虚拟地址的了,所以在分配 buffer 时就会将对应的 kmap 释放,仅释放 kmap 也不会影响到用户态中映射虚拟地址和使用,但是当这块 buffer 用户态的虚拟地址传入 RGA 驱动,驱动进行物理地址页表的转换查询时,由于该 buffer 的 kmap 已经被释放,或是无法查询到对应的页表项,或是直接访问到错误的地址导致内核 crash。
6.3 对策(FAQ Q4.4 原文)
DRM 提供了一个接口标志位:
/* keep kmap for cma buffer or alloc kmap for other type memory */
ROCKCHIP_BO_ALLOC_KMAP = 1 << 4
申请 DRM 内存时加上这个标志:
struct drm_mode_create_dumb arg;
...
arg.flags = ROCKCHIP_BO_CONTIG;
+ arg.flags = ROCKCHIP_BO_CONTIG | ROCKCHIP_BO_ALLOC_KMAP;
ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_CREATE_DUMB, &arg);
并确认 kernel 是否包含以下提交:
commit la8lee3e2d3726b9382ff2c48d08f4d837bc0143
Author: Sandy Huang <hjc@rock-chips.com>
Date: Mon May 10 16:52:04 2021 +0800
drm/rockchip: gem: add flag ROCKCHIP_BO_ALLOC_KMAP to assign kmap
最省事的规避方式:直接用 dma-buf fd 调用 RGA,不要把 DRM 物理地址 + mmap 虚拟地址混着传。
七、异步模式与 fence
7.1 基本机制
《RGA 开发指南》「3.24 同步操作」原文:
RGA 异步模式需要调用该接口等待操作完成,将返回的
release_fence_fd作为传入参数。其他 API 将形参
sync设置为 0 时,使能异步调用模式,效果相当于 opengl 中的 glFlush,如果进一步调用imsync可以达到 glFinish 的效果。
7.2 实测验证
异步对照:
for (int i = 0; i < LOOP_COUNT; i++) {
int release_fence_fd = -1;
imcopy(src, dst, 0, &release_fence_fd);
if (release_fence_fd >= 0) imsync(release_fence_fd);
}
结果:
[bench_async_copy] 1920x1080 RGBA8888 → RGBA8888 (async)
imcopy async total 83065 us, avg 830 us
830 us 比同步的 903 us 快 8%。 符合 FAQ Q1.6 的说法:RK3588 用 multicore 驱动,fence 机制针对单次请求实时处理。
7.3 注意:不是所有驱动上异步都更快(FAQ Q1.6)
原文:
RGA Device Driver、RGA2 Device Driver 由于目前 librga 的异步模式的标识符为打开的设备节点,而单例模式的 librga 一个进程只会打开一个 fd,所以
imsync()是等待该进程所有的异步模式均运行结束后才会返回。而 RGA multicore Device Driver 引入了 fence 机制,所以是针对单次请求的实时处理,不会存在这种问题。
RK3588 用的是 multicore 驱动,所以可以放心用异步。
八、多核调度:并行、核心匹配与指定核心
8.1 并行能力(FAQ Q1.12)
原文:
RGA API 是可以支持多线程/进程并行调用的,但实际硬件上是否并行执行图像操作取决于当前使用芯片搭载的 RGA 核心数量,即搭载的核心数量则为最大支持的并行任务数量,超过核心数量的任务则会进入等待状态,直到有核心进入空闲状态。因此当并行调用的数量超过了硬件最大支持的并行数量后,那么个别帧的调用将会增加等待硬件空闲的耗时。
RK3588 上有 3 颗核,所以最多并行 3 个 RGA 任务。
8.2 硬件匹配日志(FAQ Q4.6)
原文给出两段 policy 日志:
rga_policy: start policy on core = 4
rga_policy: RGA2 only support under 4G memory!
rga_policy: optional_cores = 0
rga_policy: invalid function policy
rga_policy: assign core: -1
rga_job: job assign failed
rga_policy: start policy on core = 1
rga_policy: core = 1, break on rga_check_dst
rga_policy: start policy on core = 2
rga_policy: core = 2, break on rga_check_dst
rga_policy: start policy on core = 4
rga_policy: RGA2 only support under 4G memory!
rga_policy: optional_cores = 0
rga_policy: invalid function policy
rga_policy: assign core: -1
rga_job: job assign failed
FAQ Q4.6 原文注释:
这里 core 0x1、0x2 为 RGA3 核心,0x4 为 RGA2 核心。
8.3 RK3588 专有坑:不同核心缩放算法不一致导致画面抖动(FAQ Q2.22)
原文:
由于 RK3588 比较特殊,搭载有两种 RGA 核心(一颗 RGA2,两颗 RGA3),它们在缩放算法上存在一些取数行为的差异导致结果会出现整体左上移/右下移的现象,因为在这种对显示效果有要求的场景上,建议指定核心的方式来避免出现算法差异引入的抖动问题。
指定核心的示例:
<librga_source_path>/samples/config_demo/src/rga_config_single_core_demo.cpp
<librga_source_path>/samples/config_demo/src/rga_config_thread_core_demo.cpp
两种用法:
/* 线程级配置 */
imconfig(IM_CONFIG_SCHEDULER_CORE,
IM_SCHEDULER_RGA3_CORE0 | IM_SCHEDULER_RGA3_CORE1);
/* 单任务配置 */
im_opt_t opt = {};
opt.core = IM_SCHEDULER_RGA3_CORE0;
ret = improcess(src_img, dst_img, {}, {}, {}, {}, -1, NULL, &opt, IM_SYNC);
8.4 priority / core 的使用警告(《RGA 开发指南》3.25.1 + 4.2.3)
原文两处重复强调:
priority、core 权限极高,操作不当可能导致系统崩溃或死锁,建议仅用于开发调试阶段,极度不建议在实际产品场景进行配置。
线程上下文的配置优先级低于接口的传参配置。 如果接口传参配置未配置相关参数,则本地调用使用上下文默认配置完成本地调用;如果接口传参配置相关参数,以接口传参的配置完成本次调用。
唯一的例外是上面“规避 RK3588 算法差异”这类明确必要且经过验证的场景。
8.5 一次性处理多个区域
FAQ Q2.6 原文:
RGA 在硬件上只能顺序工作即配置的一个任务工作结束和进行下一个配置的工作。因此不能一次绘制多个矩形区域,可以通过 async 模式把需要 RGA 做的工作往底层驱动配置,RGA 会将工作存储在驱动自己管理的一个工作队列中按顺序完成。
在 librga 1.9.0 版本后,增加尾缀为 array 的接口,支持配置多个矩形区域进行划线、画框、填充矩形等操作,例如
imfillArray、imrectangleArray。
九、CPU 负载为什么高?
FAQ Q1.8 原文给了三条原因:
1. 使用虚拟地址调用 RGA。 虚拟地址本身是 CPU 的访问地址,要通过当前进程的映射表转换为硬件可识别的离散的物理地址表,是通过 CPU 查询、计算的,因此会引入额外的 CPU 负载。
2. 虚拟地址是 cacheable 的。 由于使能了 cache,RGA 驱动会在硬件访问内存前后强制同步 cache 数据,因此会增加 CPU 同步 cache 和内存的负载。
3. 使用 dma-buf fd 时,分配器默认分配的是 cacheable 的 buffer。 kernel 中 dma-buf 的处理会强制同步 cache,也会存在每次调用 RGA 时有较大的 CPU 负载。该种情况下建议分配禁用 cache 的 dma-buf。
十、本篇小结
第一,性能评估公式:W × H / (px_per_cycle × freq)。RGA2 = 2 px/cycle,RGA3 = 3 px/cycle。实际耗时约为理论值的 1.1~2.1 倍,取决于内存类型。
第二,为什么实际场景比 demo 慢一倍:RGA 总线优先级最低档,带宽紧张时延迟剧增。
第三,必做优化三件套:
- 用 dma-buf fd(不用虚拟地址)
- 分配非 cache 内存
- buffer pool 轮转 + importbuffer 只调一次
第四,选对分配器:优先用 dma_heap / DRM,避开 ION / V4L2。
第五,4G 内存限制是 RK3588 隐性大坑:RGA2 的 IOMMU 只支持 32bit,color fill 和 YUV422/420 planar 必须用 4G 内内存。用 DMA_HEAP_DMA32_UNCACHED_PATH。
第六,DRM 物理地址 + mmap 虚拟地址混用会 crash:加 ROCKCHIP_BO_ALLOC_KMAP,或者干脆只用 fd。
第七,异步 fence 在 RK3588 上可用:实测 imcopy 830us vs 同步 903us。
第八,多核调度:RK3588 最多并行 3 个任务,超过就排队。不同核缩放算法有差异,必要时指定核心。但 priority / core 配置仅限调试,产品化慎用。
第九,Array 接口(librga 1.9.0+):一次配置多个矩形区域,比循环调用更高效。