
1. 为什么用RGA做图像缩放硬件加速的边界到底在哪先说结论RK3588上的RGARaster Graphic Acceleration是一块独立于CPU和GPU的二维图形加速硬件专门处理图像缩放、格式转换、旋转、裁剪这类操作。我最初接触它是因为一个很现实的需求——板子上跑着Linux系统视频流从摄像头进来是NV12格式分辨率是4K但下游算法模块只需要1080P而且显示链路还要另一路720P的预览。如果全部用CPU来做像素拷贝和缩放4K NV12转1080P一次大概要消耗几十毫秒算法一跑起来CPU占用率直接飙到30%以上。用RGA之后同样操作耗时降到2毫秒以内CPU占用几乎可以忽略。这块硬件不用就是浪费。很多做RK3588开发的朋友问我的第一句话是RGA和MPP到底什么关系这里先把这个理清楚。MPPMedia 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的老版本APIrga_open/rga_close这种和新版本APIim2d系列在底层实现上有差异而不同芯片平台对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如果你用的是CMaketarget_link_libraries里的顺序也要注意这里的顺序就是最终传给链接器的顺序。另外在Ubuntu 20.04上交叉编译时还遇到一个特殊情况就是pkg-config找不到libdrm.pc导致-ldrm实际没生效。这个通过确认工具链sysroot里的pkgconfig路径是否配置好来解决。3.3 错误三运行时报RGA_QUERY_FAILEDImageZoom功能直接不工作这是最让人头疼的错误因为编译全通过一跑就挂。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接口为例核心逻辑分成三步第一步声明源和目标bufferrga_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.10librga 1.9.0芯片跑在标准频率下未做任何调频。缩放操作全部用im2d同步模式。操作输入格式输出格式耗时备注4K NV12 - 1080P NV123840x21601920x10801.8ms纯缩放4K NV12 - 1080P RGBA3840x21601920x10802.3ms缩放格式转换4K NV12 - 720P NV123840x21601280x7201.5ms纯缩放1080P NV12 - 4K NV121920x10803840x21601.9ms放大1080P NV12 - 4K NV12 旋转90度1920x10803840x21602.6ms缩放旋转1080P RGB888 - 1080P NV121920x10801920x10801.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当成了NV21Y和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_ASYNCim2d_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上少踩几个坑把图像缩放这件事真正做到又快又稳。