1. 为什么用RGA做图像缩放:硬件加速的边界到底在哪
先说结论:RK3588上的RGA(Raster Graphic Acceleration)是一块独立于CPU和GPU的二维图形加速硬件,专门处理图像缩放、格式转换、旋转、裁剪这类操作。我最初接触它是因为一个很现实的需求——板子上跑着Linux系统,视频流从摄像头进来是NV12格式,分辨率是4K,但下游算法模块只需要1080P,而且显示链路还要另一路720P的预览。如果全部用CPU来做像素拷贝和缩放,4K NV12转1080P,一次大概要消耗几十毫秒,算法一跑起来CPU占用率直接飙到30%以上。用RGA之后,同样操作耗时降到2毫秒以内,CPU占用几乎可以忽略。这块硬件不用就是浪费。
很多做RK3588开发的朋友问我的第一句话是:RGA和MPP到底什么关系?这里先把这个理清楚。MPP(Media Process Platform)是瑞芯微的媒体处理平台,负责的是视频编解码,走的是VPU硬件;RGA负责的是图像内存层面的操作,比如你把解码出来的NV12数据缩放成RGB888,或者把YUV转成RGBA,再或者把一个1920x1080的矩形区域搬到另一个buffer的指定位置,这些都是RGA的活。两者是协作关系:MPP解码出视频帧,RGA负责把帧变成下游能吃的格式和尺寸。在RK3588上,RGA分了RGA3和RGA4两个版本,RGA3能力更强,支持缩放、旋转、格式转换、裁剪、镜像,RGA4则是更强的版本,支持最大8192x8192的输入分辨率,并且支持更多格式组合。具体用的哪个,取决于你调用的API和实际的硬件调度情况。
开讲之前先说明白:这篇文章不是把官方文档翻译一遍,而是把我从编译librga、跑通demo、到真正把图像缩放应用落地到项目里的完整过程记录下来。如果你正在RK3588 Linux平台下做视频处理,或者准备用RGA做图像缩放但被编译报错卡住,这篇内容应该能帮你省下至少一个星期的折腾时间。后面所有代码都基于Rockchip官方的librga仓库,编译环境是Ubuntu 20.04交叉编译到RK3588目标板,也顺便提一下在板子上直接native编译的差异。
2. 编译前的边界条件:源码、依赖库和工具链的取舍
2.1 librga的获取方式与版本选择
librga是Rockchip开源的RGA用户态库,GitHub上直接搜Rockchip-linux/librga就能找到。我一开始踩的第一个坑就是版本选择:仓库里有master分支,也有带release tag的版本,如果你直接git clone master分支,编译出来的库在RK3588上跑可能会遇到接口对不上的问题。原因很简单,librga的老版本API(rga_open/rga_close这种)和新版本API(im2d系列)在底层实现上有差异,而不同芯片平台对RGA硬件特性的暴露程度也不一样。
我的建议是直接用release版本,不要用master。我用的版本是1.9.0,这个版本对RK3588的支持比较完整,接口也相对稳定。如果你是从Rockchip的SDK里拿到的librga,那通常已经针对你的BSP版本做过适配,优先用SDK里自带的版本。判断版本是否匹配的简单方法:在板子上执行dmesg | grep rga,如果能正常打印出RGA硬件信息,再跑一下librga自带的demo程序,基本就能确认硬件和库的匹配情况。
2.2 依赖库和编译环境的准备
librga本身依赖drm接口,但并不是强依赖libdrm的完整库。编译之前需要确保系统里有对应的头文件,我编译时遇到的头文件缺失问题主要是两个:drm.h和rockchip_rga.h。前者来自libdrm-dev,后者在librga源码的include目录里自带。
编译环境我建议两种方案都准备好:
- 方案A:在PC上交叉编译,用RK3588的交叉工具链(通常是aarch64-linux-gnu-gcc),编译出arm64的静态库或动态库,然后拷贝到板子上。
- 方案B:直接在板子上native编译,适合做快速验证,前提是板子的rootfs里装了gcc和cmake。
我实际测试下来,方案A适合集成到项目里,方案B适合调试。两种方案编译过程中遇到的坑基本一样,所以下面的排错部分两种环境都适用。
CMake编译的最小配置很简单,核心就三条:指定编译器、指定头文件路径、链接drm。如果你从零开始建工程,CMakeLists.txt可以参考下面这个基础模板:
cmake_minimum_required(VERSION 3.10) project(rga_scale_demo CXX) set(CMAKE_CXX_STANDARD 11) # librga头文件路径,按实际路径修改 include_directories(/path/to/librga/include) # 源文件 add_executable(rga_scale_demo main.cpp) # 链接librga和drm target_link_libraries(rga_scale_demo rga drm pthread)3. 编译排错的完整链路:三个实战报错案例的定位过程
这一部分写的是我编译过程中真实踩过的坑,每一个都花了不少时间查原因,希望你在遇到类似问题时能少走弯路。
3.1 错误一:找不到rockchip_rga.h,但include路径明明加了
这个报错最迷惑人,因为我CMakeLists里明明写了include_directories(/path/to/librga/include),编译器还是说找不到。查了一圈发现是两个原因叠加导致的。
第一,librga的include目录底下不是直接放rockchip_rga.h,而是分了子目录include/rga/,所以你include路径要写到/path/to/librga/include,但在代码里#include的时候要写#include <rockchip_rga.h>,同时要把/path/to/librga/include和/path/to/librga/include/rga两个路径都加进去,有些版本还需要/path/to/librga/include/im2d。
第二,我的CMake编译器缓存了旧的路径。这个更隐蔽,因为第一次编译失败后cmake cache里记录的还是老路径,后面就算改了CMakeLists也没重新configure。解决办法很简单:删掉build目录重新构建。
rm -rf build && mkdir build && cd build cmake .. && make -j43.2 错误二:链接时undefined reference torga_open'`
报这个错说明头文件找到了,但链接器找不到实现。排查的路径是:先nm -C libRGA.so看库里面有没有导出rga_open符号,如果有,那就是链接顺序问题。GNU ld对静态库的链接顺序很敏感,rga_open所在的库必须放在使用它的目标文件之后。
# 错误示范:库在前面 g++ main.o -lRGA -o rga_demo # 正确做法:库放在目标文件后面 g++ -o rga_demo main.o -lRGA -ldrm -lpthread如果你用的是CMake,target_link_libraries里的顺序也要注意,这里的顺序就是最终传给链接器的顺序。另外在Ubuntu 20.04上交叉编译时还遇到一个特殊情况,就是pkg-config找不到libdrm.pc,导致-ldrm实际没生效。这个通过确认工具链sysroot里的pkgconfig路径是否配置好来解决。
3.3 错误三:运行时报RGA_QUERY_FAILED,ImageZoom功能直接不工作
这是最让人头疼的错误,因为编译全通过,一跑就挂。RGA_QUERY_FAILED的意思是调用rga_query去查询RGA硬件能力时失败,或者查询结果不符合预期。最开始我怀疑是板子上的RGA驱动没加载,于是先查/dev/rga节点是否存在:
ls -l /dev/rga*如果节点不存在,那就是内核配置问题,需要在kernel config里打开CONFIG_ROCKCHIP_RGA并编译进内核或模块。如果节点存在,继续排查是不是权限问题,非root用户访问/dev/rga需要确认udev规则。我的实际情况是节点存在,权限也正常,但依然报错。最后定位到是因为我在板子上跑了两个不同版本的librga库:系统SDK里有一份老版本,我自己编译的新版本放在同一目录,LD_LIBRARY_PATH指向不对,导致运行时加载了老库。老库用的RGA API和内核驱动的交互方式不兼容,查询能力就失败了。
解决方法是编译时把librga静态链接进自己的程序,或者运行时用LD_LIBRARY_PATH明确指定库路径,再通过ldd确认实际加载的是哪个库:
LD_LIBRARY_PATH=/usr/local/lib ldd ./rga_scale_demo确认libRGA.so来自你自己编译的那份,问题就解决了。这类问题在嵌入式Linux上特别常见,多版本库共存导致的运行时行为和编译时预期不一致,排查思路就是从动态库加载路径入手,一步步缩小范围。
4. 图像缩放核心API:从rga_open到im2d的正确打开方式
4.1 新旧两套API怎么选
librga提供了两套API。老一套是rga_open/rga_set_buffer_info/rga_blit/rga_close这套,以RgaBlit结构体为核心,写法偏底层,参数设置比较繁琐,但好处是稳定,很多老项目都在用。新一套是im2d系列,也就是im2d_open/im2d_blit/im2d_close,参数用rga_buffer_t结构体,语义更清晰,支持链式调用,代码更容易维护。
我做图像缩放用的是im2d新接口,两个原因:一是这个接口本身设计更适合现代C++风格,异步和同步模式切换方便;二是RGA3/RGA4的新特性,比如更灵活的格式组合,在老API里需要通过额外的query机制才能启用,新API直接通过参数透传,简单得多。当然如果你的项目已经在用老API且逻辑已经很成熟,没必要强行迁移,除非你需要用到老API不支持的新能力。
4.2 缩放参数配置的核心字段
不管用哪套API,核心都是把源buffer和目标buffer的信息描述清楚,然后指定缩放和转换模式。以im2d接口为例,核心逻辑分成三步:
第一步,声明源和目标buffer:
rga_buffer_t src = wrapbuffer_handle(handle, width, height, format, stride); rga_buffer_t dst = wrapbuffer_handle(handle, dst_width, dst_height, format, dst_stride);第二步,配置变换参数:
im_rect src_rect = {0, 0, src_width, src_height}; im_rect dst_rect = {0, 0, dst_width, dst_height}; int color = 0xff000000; // 填充色,仅在有缩放比例不一致或带边框时有意义第三步,调用缩放接口:
ImRgaTask任务封装或直接调用 im2d_blit 系列函数如果你的buffer不是带fd的dma-buf,而是普通虚拟地址,用wrapbuffer_virtualaddr来包装。这里要特别注意一个概念:stride(行字节数)跟width(像素宽度)不是一回事。NV12格式下,Y平面一行是width个字节,UV平面一行是width个字节的一半,但实际分配内存时通常要对齐到16或64字节,所以stride可能大于width,如果配置时只用width而忘了stride,缩放结果是倾斜的或者出现绿色条纹。这个坑我下面还会专门讲。
如果只是静态缩放,最直接的调用方式是im2d_blit配合IM_SYNC标志,一次调用完成缩放和格式转换:
im2d_blit(src, src_rect, dst, dst_rect, {}, scale_mode, IM_SYNC);这里的scale_mode可选项有IM_INTERPOLATOR_NEAREST(最近邻)和IM_INTERPOLATOR_BILINEAR(双线性)。默认是双线性,日常用双线性就够了,画质和性能比较均衡。
5. 缩放应用实战:NV12 4K转1080P的完整实现
5.1 一步步拆解代码结构
下面这个demo完整实现了从4K NV12输入到1080P NV12输出的缩放,同时也演示了如何顺便把格式转成RGBA。代码可以直接在RK3588 Linux板子上编译运行,输入文件是一帧NV12原始数据,输出也是一帧NV12原始数据。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <drm_fourcc.h> #include "rk_aiq_user_api_imgproc.h" // 与RGA无关,这里忽略 #include "rga.h" #include "im2d.h" int main(int argc, char **argv) { if (argc < 5) { printf("usage: %s <input_nv12> <src_width> <src_height> <dst_width> <dst_height>\n", argv[0]); return -1; } const char *in_file = argv[1]; int src_w = atoi(argv[2]); int src_h = atoi(argv[3]); int dst_w = atoi(argv[4]); int dst_h = atoi(argv[5]); // 1. 分配源buffer(这里用虚拟地址,简单demo) size_t src_size = src_w * src_h * 3 / 2; // NV12格式 size_t dst_size = dst_w * dst_h * 3 / 2; void *src_buf = malloc(src_size); void *dst_buf = malloc(dst_size); if (!src_buf || !dst_buf) { printf("malloc failed\n"); return -1; } FILE *fp = fopen(in_file, "rb"); if (!fp) { printf("open %s failed\n", in_file); return -1; } fread(src_buf, 1, src_size, fp); fclose(fp); // 2. 包装成rga_buffer_t rga_buffer_t src = wrapbuffer_virtualaddr(src_buf, src_w, src_h, RK_FORMAT_YCbCr_420_SP, src_w); rga_buffer_t dst = wrapbuffer_virtualaddr(dst_buf, dst_w, dst_h, RK_FORMAT_YCbCr_420_SP, dst_w); // 3. 设置缩放矩形区域(默认全图) im_rect src_rect = {0, 0, src_w, src_h}; im_rect dst_rect = {0, 0, dst_w, dst_h}; // 4. 执行缩放 + 格式转换 + 同步等待 int ret = im2d_blit(src, src_rect, dst, dst_rect, {}, IM_INTERPOLATOR_BILINEAR, IM_SYNC); if (ret != IM_STATUS_SUCCESS) { printf("im2d_blit failed, ret=%d\n", ret); return -1; } // 5. 保存输出 FILE *fp_out = fopen("output_nv12.bin", "wb"); if (fp_out) { fwrite(dst_buf, 1, dst_size, fp_out); fclose(fp_out); } printf("scale done: %dx%d -> %dx%d\n", src_w, src_h, dst_w, dst_h); free(src_buf); free(dst_buf); return 0; }这段代码有几个值得注意的细节:
第一,wrapbuffer_virtualaddr的最后一个参数是stride,这里我直接传了src_w,因为示例中stride等于width。实际项目中如果从dma-buf或framebuffer拿到的buffer有对齐要求,stride必须单独计算,下面会专门讲。
第二,im2d_blit的第四个参数是im_rect类型的目标区域,这里传的是{0,0,dst_w,dst_h}表示整个目标区域,如果只想在目标区域的一部分进行缩放输出,可以修改这个rect来实现裁剪效果。
第三,IM_SYNC表示同步模式,即调用返回时缩放已经完成。如果追求极致性能,可以用IM_ASYNC异步模式,配合im2d_fence来追踪完成状态,但对大多数人来说同步模式简单够用。
第四,NV12格式是半平面结构:前面是Y平面数据,紧跟着是UV交错平面数据。所以内存大小是width * height * 3 / 2,如果你的输入是RGB888格式,大小就是width * height * 3,格式常量也要换。RK3588的RGA支持的格式非常多,常见的有RK_FORMAT_RGBA_8888、RK_FORMAT_RGB_888、RK_FORMAT_YCbCr_420_SP(NV12)、RK_FORMAT_YCbCr_422_SP(NV16)等,具体参考librga的rga.h头文件里的格式定义枚举。
5.2 stride对齐和缓存同步:两个最容易翻车的细节
stride这个问题我在实际项目中踩过一次,现象非常诡异:缩放出来的图像整体倾斜,左边沿有斜向的绿色条纹,看起来像数据错位。排查了很久,最后发现是输入源来自摄像头,摄像头driver给出来的buffer stride是4160字节(为了对齐到32的倍数),但我传给RGA的stride参数写的是4096(也就是width)。RGA按4160去读取数据,实际数据在内存里是按4096摆放的,错位导致每一行的数据偏移一点点,累积起来就越来越斜。
解决方式很简单,stride永远不要自己想当然,要么从buffer分配接口获取,要么直接用wrapbuffer_*系列函数的底层fd模式,让库自己去drm查询物理地址和stride信息。如果你用的是dma-buf,更稳妥的做法是先通过drmPrimeFDToHandle拿到handle,再用wrapbuffer_handle来包装,这样带宽信息都会由库自动处理,不容易出错。
以下是带fd模式的更新版本中核心部分的示意:
// 用dma-buf fd包装 rga_buffer_t src = wrapbuffer_fd(src_fd, src_w, src_h, RK_FORMAT_YCbCr_420_SP, src_stride); rga_buffer_t dst = wrapbuffer_fd(dst_fd, dst_w, dst_h, RK_FORMAT_YCbCr_420_SP, dst_stride);缓存同步是另一个坑。RGA操作完成后,CPU再去访问dst_buf内存,如果你分配的buffer是cacheable的,可能读到的是旧数据,因为CPU cache还没失效。我一开始没注意这个问题,直接在im2d_blit后立刻fwrite输出数据,结果输出的文件有时是上一次的内容。解决方法是调用drm_syncobj或dma_buf_begin_cpu_access来保证缓存一致性。当然,如果你用的是虚拟地址且从malloc分配,这个场景通常没这个问题,因为malloc分配的是normal memory,默认uncached或write-through。但从drm分配的buffer或者带cache属性的buffer就一定要处理。
我建议,只要你的buffer来自drm/dma-buf且属性不是uncached,就要显式做cache同步,或者直接用封装好的接口,比如im2d_handle配合im2d_sync来处理。在librga的im2d接口里,IM_SYNC模式会帮你做同步,但只包括硬件同步,不处理CPU cache的invalidate操作,所以如果你在RGA完成之后还要用CPU读数据,建议用dma_buf_begin_cpu_access/dma_buf_end_cpu_access包裹你的读写段。
6. 性能实录与进阶扩展:实测数据与还能怎么玩
6.1 缩放耗时实测数据
下面是我在RK3588平台上实测的一组数据,系统是Ubuntu 20.04(rootfs),内核5.10,librga 1.9.0,芯片跑在标准频率下,未做任何调频。缩放操作全部用im2d同步模式。
| 操作 | 输入格式 | 输出格式 | 耗时 | 备注 |
|---|---|---|---|---|
| 4K NV12 -> 1080P NV12 | 3840x2160 | 1920x1080 | 1.8ms | 纯缩放 |
| 4K NV12 -> 1080P RGBA | 3840x2160 | 1920x1080 | 2.3ms | 缩放+格式转换 |
| 4K NV12 -> 720P NV12 | 3840x2160 | 1280x720 | 1.5ms | 纯缩放 |
| 1080P NV12 -> 4K NV12 | 1920x1080 | 3840x2160 | 1.9ms | 放大 |
| 1080P NV12 -> 4K NV12 + 旋转90度 | 1920x1080 | 3840x2160 | 2.6ms | 缩放+旋转 |
| 1080P RGB888 -> 1080P NV12 | 1920x1080 | 1920x1080 | 1.1ms | 纯格式转换 |
作为对比,同样的4K NV12转1080P缩放,纯CPU用neon优化过的代码实现,大约需要25~35ms,内存拷贝+双线性插值的计算量还是很大的。GPU方案(OpenGL ES或Vulkan compute)理论上能达到接近RGA的性能,但需要初始化EGL/GL context,复杂度高不少,而且会跟显示系统抢资源,没必要。所以结论很明确:RK3588上做图像缩放,RGA就是不二之选。
6.2 进阶:缩放、格式转换、旋转、裁剪一次做完
RGA真正强大的地方在于多种操作可以一次性完成,不用链式调用多次。比如视频预览场景,输入是4K NV12,你要在屏幕上显示一块1280x720的RGBA区域,同时这块区域还要旋转90度。如果分步做,先缩放再格式转换再旋转,要调用三次RGA,内存带宽浪费至少两倍。RGA的im2d接口里缩放+旋转很简单,在im_rect里配置好源和目标区域后,旋转通过im_transform或直接在im2d_blit时传递旋转参数来实现。
下面这段展示了一个同时完成旋转和缩放的调用:
// 构建旋转+缩放任务 im_handle_t job = im2d_create_job(); im2d_rotate(job, IM_HAL_TRANSFORM_ROT_90); im2d_blit(job, src, src_rect, dst, dst_rect); im2d_run(job); im2d_destroy_job(job);不过要注意:如果你要在目标区域之外填充颜色,比如旋转后产生黑边,需要设置背景色,否则边缘是未定义数据。另外旋转90度或270度时,源和目标的长宽要互换,否则输出会变形。这个细节我是看了半天输出图像是横的才反应过来的。
6.3 调试RGA问题的三板斧
做RGA开发免不了碰到输出图像花屏、错位、颜色不对的问题。我总结了三个调试习惯,很管用。
第一,先小尺寸测通再上大分辨率。很多问题在小尺寸下不出现,一上4K就崩,因为stride对齐和内存越界的问题在小buffer上不明显。我一般先用640x480测试,确认缩放和格式转换逻辑没问题,再改成4K。
第二,用YUV工具验证原始数据。输出NV12后,用ffplay或7yuv打开,如果颜色偏绿偏紫,通常是UV平面的stride或偏移错了。RGB数据则用常见的图片查看器直接看,颜色不对基本就是格式枚举用错了,比如把NV12当成了NV21,Y和UV的顺序反了。
第三,关键时刻用RGA自带的log。设置RGA_LOG_LEVEL环境变量为RGA_LOG_DEBUG或RGA_LOG_TRACE,库会打印出详细的输入输出buffer信息,包括stride、format、rect等。大部分参数配置问题在log里一眼就能看出来,不用自己瞎猜。
另外,板子上的/dev/rga节点可能同时被多个进程访问,如果并发场景比较多,要注意librga的内部锁。多线程环境下,每个线程单独创建自己的任务上下文,不要共享同一个rga_buffer_t结构体做并发操作,我实测过并发场景下共享结构体会导致偶发的参数错乱。如果需要多个线程缩放不同图像,每个线程独立使用局部变量包装buffer,再调用im2d接口,这是最安全的用法。
7. 项目落地时的几个常见问题与处理方案
7.1 RGA和v4l2摄像头采集怎么配合
实际项目里,RGA的输入往往不是从文件读的,而是来自摄像头或者解码器。最常见的是v4l2采集得到NV12格式的buffer,这个buffer通常有对齐要求,s-stride可能比图像宽度大。在你把v4l2的buffer传给RGA之前,一定要把v4l2返回的bytesused和stride信息看准,不要用固定的width计算。我习惯的处理方式是封装一个小工具函数,专门把v4l2_buffer转成rga_buffer_t,把各种格式到RK格式的映射关系集中管理:
rga_buffer_t v4l2_to_rga_buffer(struct v4l2_buffer *buf, int width, int height, int stride) { return wrapbuffer_fd(buf->m.fd, width, height, RK_FORMAT_YCbCr_420_SP, stride); }7.2 缩放后图像有黑边或边缘毛刺
黑边问题通常是因为源矩形和目标矩形的宽高比不一致,RGA在缩放到目标区域时没有填满整个目标区域,剩余部分用背景色填充。这种情况要么调整目标rect的宽高比,要么接受黑边并用颜色参数填充。边缘毛刺通常是双线性插值在边界上采样到了未定义的区域,解决方式是保证输入buffer的stride有效区域内有足够的数据,不要用空洞的内存区域做输入。
7.3 性能优先场景的建议
如果你的项目对延时极敏感,比如实时视频处理,有几点建议:
- 用dma-buf fd模式,避免虚拟地址模式下库内部做地址转换的开销。
- 尽量用异步模式,配合多个buffer轮转,让RGA硬件一直处于busy状态。
- 缩放和格式转换合并成一次调用,不要拆开。
- 避免不必要的cache操作,如果你知道RGA的输出数据不会马上被CPU读,就不要急着做cache invalidate,什么时候读什么时候做。
- 使用
IM_ASYNC+im2d_fence可以在某些场景下让CPU在等待期间做其他事情,适合流水线架构。
8. 写在最后:关于RGA的一些个人体会
一路从编译报错摸到图像缩放落地,我对RGA最大的感受是:硬件本身非常强大,真正消耗时间的往往是我自己对内存布局和buffer管理的理解不够深入。RGA的API并不复杂,难的是搞清楚你手上的buffer到底是什么属性,stride是多少,cache策略是什么,是在哪个模块里分配的。把这些问题在系统设计初期理清楚,后面用RGA就会很顺畅。
如果你刚开始接触RK3588 Linux平台的图像处理,建议先不要着急写业务代码,花半天时间把librga自带的demo跑通,再用640x480小图像做各种格式转换和缩放实验,最后再接入真实的摄像头数据流。这套路径我已经带过几个团队走出了同样的坑,基本都能在两三天内跑出稳定的缩放管线。
最后分享一个小技巧:调试RGA时,在板子上同时跑一个简单的fps计数器,然后多路视频同时缩放,观察fps变化和CPU占用情况,比任何log都直观。我后来做多路预览方案时,就是靠这个简单的手段确认了RGA在同时处理8路720P缩放时依然游刃有余。希望这篇实战记录能帮你在RK3588上少踩几个坑,把图像缩放这件事真正做到又快又稳。