RK3588多路视频零拷贝实战:MPP硬解码+RGA加速全流程

发布时间:2026/9/4 12:22:45
RK3588多路视频零拷贝实战:MPP硬解码+RGA加速全流程 1. 项目背景多路视频处理的性能之痛做嵌入式视频处理的朋友应该都有这种体会RK3588 这颗芯片算力看着很猛8 核 CPU、6 TOPS NPU、8K VPU但真把多路视频流接进来做处理时CPU 占用率蹭蹭往上涨NPU 还没干正事系统先卡了一半。问题出在哪儿大部分人第一反应是解码器不够快其实不然——真正拖后腿的往往是视频数据在内存里的搬运路径。拿我之前做的一个项目来说8 路 1080P 摄像头接入需要做实时检测、ROI 裁剪、缩放、格式转换后送给 NPU 推理。最初方案是 CPU 软解 OpenCV 处理结果是 8 核全部跑满每路帧率不到 15 FPSCPU 温度直接飙到 85℃。后来换到 RK3588 的 VPU 硬解码解码本身几乎不占 CPU 了但 OpenCV 的resize、cvtColor这些操作依然吃 CPU——因为硬解出来的视频帧默认是 NV12 格式存放在 DDR 里OpenCV 做一次颜色空间转换就是逐像素的内存读写8 路叠加起来CPU 是顶不住的。这个项目最终能被压下来靠的是三个关键词MPPRockchip Media Process Platform、RGARaster Graphic Acceleration2D 硬件加速和零拷贝。这篇文章我把我实际踩坑和调优的过程整理出来从原理到代码到性能数据做成一份可以直接参考的实战笔记。适合正在用 RK3588 做视频监控、AI 边缘计算、多路 RTSP 拉流处理的朋友也适合那些刚接触 MP 和 RGA、想搞清楚它们到底怎么配合使用的人。2. 整体方案设计与技术选型思路2.1 为什么非要用 MPP 硬解码而不是软解或第三方库先看一张简化到不能再简化的数据流图我对照实际硬件结构来说明[摄像头] - [网络传入] - [MPP硬解] - [RGA处理] - [NPU/内存/显示]RK3588 的 VPU 单元是硬核的熵解码 像素重建处理单元H.264/H.265 的解码能力在 8K 分辨率下也能维持较高的帧率最关键的是解码过程中不再吃 CPU 指令周期。软解也就是 FFmpeg 默认的h264解码器走 CPU 跑单路 1080P 可能就要占 1.5~2 个核8 路就是 12~16 个核RK3588 总共就 8 核算力直接耗尽。有人会问FFmpeg 不是也带硬解吗RK3588 上 FFmpeg 确实可以通过rkmpp补丁或 Rockchip 发布的 MPP 版本调用硬解但核心问题在于——FFmpeg 的默认输出路径会额外做一次 NV12 到内部 frame 格式的拷贝。你用它读到一帧 PIX_FMT_NV12内部已经过了一次 memcpy传到后面的 RGA 又要再拷贝一次。三次拷贝下来带宽消耗不可小觑。所以我们这里是个组合拳思路MPP 负责解码输出 DMA bufferdrm buffer直接交给 RGA 做处理全程避免 CPU 参与像素搬运。MPP 对底层 buffer 的掌控力最强RGA 可以接受 dma-buf fd 并直接操作这两者配合才能做整套零拷贝链路。2.2 RGA 到底能加速什么别指望它干所有事RGA 是 Rockchip 的 2D 图形加速引擎它的核心操作包括缩放、旋转、格式转换、裁剪、合成、填充。我实测下来它最擅长的是以下场景NV12 → RGB/BGR颜色空间转换AI 前处理最常见任意尺寸缩放包括非等比缩放ROI 裁剪把画面中需要的区域切出来多路拼接虽然拼接我也常用它做但大分辨率下带宽有上限旋转、镜像90/180/270 度政务、安防摄像头常常装歪了需要纠正图像叠加YUV格式的OSD比如时间戳、水印但是要注意RGA 不是万能的。它不是 GPU 那样的通用计算设备不能跑深度学习算子它的带宽和算力也有限制。RK3588 有两路 RGARGA2 和 RGA3单路实测 1080P 缩放大概能跑到 600 FPS这已经碾压 CPU 了但如果你的业务是跨多路超高清合成比如 4 路 4K 拼 8K带宽就跑不满需要后续再做流水线调度优化。2.3 零拷贝链路MPP 和 RGA 怎么配合才算真零拷贝这里“零拷贝”不是指物理上一个拷贝都不做RGA 处理后结果总归要写内存而是指避免 CPU 为了搬运数据而做的 memcpy避免用户态和内核态之间反复切换导致的数据复制。RK3588 上的典型零拷贝链路是这样MPP 解码时MppBuffer通过mpp_buffer_get_with_fd获取关联的 DMA fd。RGA 接收一个rga_buffer_t可以用wrapbuffer_fd_t把 fd 包进来设置color_space等参数。调用im2d或者rga2d的 API指定输入 fd、输出 fd输出的 fd 可以来自另一块 MPP buffer也可以来自 DRM buffer让 RGA 硬件引擎直接读源写目标。这样每一帧数据从解码器出来到交给 RGA再到进入下一环节NPU、编码器或显示中间没有 CPU 碰过像素。NPU 侧 RKNN 也支持fd输入模式可以直接拿 RGA 输出的 fd 去做推理这是后话。提示我验证过真正的零拷贝链路如果做得好8 路 1080P 的解码格式转换缩放CPU 占用率能控制在 20% 以下甚至更低。对比软解方案这就是一个数量级的差距。3. 环境准备与关键依赖这个项目的主要工程环境基于 RK3588 开发板我用的是 RK3588S 核心板 底板方案系统是 Ubuntu 21.04 aarch64 的 Rockchip 官方 BSP但只要你的板子具备完整的 MPP 和 RGA 驱动以下方法大同小异。3.1 检查系统与驱动在开始之前我建议先确认三件事避免后面跑了半天发现基础环境不对# 检查芯片型号 cat /proc/device-tree/model # 检查系统版本 uname -a # 检查 /dev/dri 节点是否存在正规 BSP 一般都有 ls /dev/dri/ # 检查 RGA 节点 ls /dev/rga如果你看到/dev/rga存在说明 RGA 驱动已加载。如果没有确认内核配置里有没有开启CONFIG_ROCKCHIP_RGA和CONFIG_VIDEO_ROCKCHIP。RK3588 的 MPP 跑在用户态调用 vpu_service也存在/dev/mpp_service节点ls /dev/mpp_service正常 BSP 都会有这些节点。没有的话就得回头查内核设备树和固件了。3.2 编译安装 MPP 和 RGA 库直接在板子上编译装库的方式如下推荐用 Rockchip 官方仓库的 release 分支# 拉取 MPP git clone https://github.com/rockchip-linux/mpp.git cd mpp mkdir build cd build cmake -DCMAKE_INSTALL_PREFIX/usr/local -DRKPLATFORMON .. make -j$(nproc) sudo make install # 拉取 RGA git clone https://github.com/rockchip-linux/linux-rga.git cd linux-rga mkdir build cd build cmake -DCMAKE_INSTALL_PREFIX/usr/local .. make -j$(nproc) sudo make install编译完成后头文件分别位于/usr/local/include/rockchipmpp 的头文件通常是rga.h、im2d.h这类链接库是librockchip_mpp.so、librga.so。如果你用的是 buildroot 或 yocto 的 SDK也可以直接在 SDK 里打开相关选项然后直接交叉编译方便打包进 rootfs。我强烈建议第一次做验证时直接在板子上编译环境简单出了问题容易定位。3.3 测试 RGA 是否真的可用库装好了先写个最简单的 RGA 缩放测试看是否正常。这个测试主要确认驱动和用户态库之间是否畅通我的示例是用 C 来做生产环境里也比较适合 C/C 这一套。#include im2d.h #include rga.h #include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/mman.h int main() { int width 1920, height 1080; int dst_w 640, dst_h 640; int src_size width * height * 3 / 2; // NV12 int dst_size dst_w * dst_h * 3 / 2; // NV12 int src_fd rga_buffer_alloc(src_size); int dst_fd rga_buffer_alloc(dst_size); rga_buffer_t src wrapbuffer_fd_t(src_fd, width, height, RK_FORMAT_YCbCr_420_SP); rga_buffer_t dst wrapbuffer_fd_t(dst_fd, dst_w, dst_h, RK_FORMAT_YCbCr_420_SP); im_rect src_rect {0, 0, width, height}; im_rect dst_rect {0, 0, dst_w, dst_h}; int ret imresize(src, dst, src_rect, dst_rect, IM_SYNC); if (ret ! IM_STATUS_SUCCESS) { printf(RGA resize failed: %d\n, ret); return -1; } printf(RGA resize test OK!\n); return 0; }这段代码里有两个点要注意rga_buffer_alloc是我封装的一个函数底层是调用drmPrimeToFd或/dev/dma_heap来分配 buffer具体实现后面会给出。RGA 的IM_SYNC是同步模式也就是调用完成后等 RGA 引擎完成再返回。调试时常用它生产环境为了并发吞吐通常会改成IM_ASYNC或者用 fence 机制im_handle。这个测试如果在板子上打印出RGA resize test OK!就说明 RGA 通路基本通了。要是出现failed优先检查权限和 fd 是否有效。4. 核心代码实现MPP 解码 RGA 处理全流程4.1 MPP 解码器的初始化与回调逻辑MPP 的用户态接口比较繁琐核心是MppCtx、MppPacket、MppFrame和MppBuffer这四类对象。解码流程是这样持续向解码器送入编码后的数据包ES 流解码器内部完成解码通过回调或mpp_frame_get取出解码后的帧我们先看初始化#include mpp.h #include mpp_buffer.h #include mpp_frame.h #include mpp_packet.h // 全局 MppCtx mpp_ctx nullptr; MppApi *mpi nullptr; MppBufferGroup frm_grp nullptr; MppBufferGroup pkt_grp nullptr; void mpp_decode_init() { mpp_create(mpp_ctx, mpi); // 配置解码器类型为 H.264 MppDecCfg cfg; mpp_dec_cfg_init(cfg); mpp_dec_cfg_set_u32(cfg, type, MPP_VIDEO_CodingAVC); mpi-control(mpp_ctx, MPP_DEC_SET_CFG, cfg); // 帧缓冲组用 dma buffer 以便 RGA 直接使用 mpp_buffer_group_get_internal(frm_grp, MPP_BUFFER_TYPE_DRM); mpi-control(mpp_ctx, MPP_DEC_SET_EXT_BUF_GROUP, frm_grp); mpp_init(mpp_ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC); }有一点比较重要如果你没设置MPP_DEC_SET_EXT_BUF_GROUPMPP 内部会用默认方式分配 buffer后续想拿到 fd 给 RGA 比较绕。设为MPP_BUFFER_TYPE_DRM后MppBuffer内部对应的就是 DRM 的 dma fd方便一路传给下游。解码取帧一般是先解码一个 packet然后循环取int decode_one_frame(uint8_t *packet_data, size_t data_size) { MppPacket packet nullptr; mpp_packet_init(packet, packet_data, data_size); mpi-decode_put_packet(mpp_ctx, packet); MppFrame frame nullptr; RK_U32 eos 0; while (1) { RK_S32 ret mpi-decode_get_frame(mpp_ctx, frame); if (ret ! MPP_OK) { mpp_packet_deinit(packet); return -1; } if (frame) { RK_U32 width mpp_frame_get_width(frame); RK_U32 height mpp_frame_get_height(frame); RK_U32 hor_stride mpp_frame_get_hor_stride(frame); RK_U32 ver_stride mpp_frame_get_ver_stride(frame); MppBuffer fb mpp_frame_get_buffer(frame); int fd mpp_buffer_get_fd(fb); // 到这里fd width height hor_stride 就是 RGA 的输入材料 rga_process(fd, width, height, hor_stride, ver_stride); mpp_frame_deinit(frame); break; } if (eos) break; } mpp_packet_deinit(packet); return 0; }这里有个细节让我踩过坑mpp_frame_get_width和mpp_frame_get_hor_stride不一定相等。比如 1920x1080 的视频hor_stride 可能是 1920也可能是 2048 或 2176看码流和平台。RGA 处理时必须用 hor_stride 而不是 width否则图像会拉伸或错位。我建议定义一个结构体专门传递 stride 信息从 MPP 到 RGA 一路保持一致。4.2 RGA 处理线程的设计与实现RGA 处理不要和解码放在同一个线程里——MPP 的decode_get_frame里面如果阻塞太久会导致解码输入队列积压丢帧甚至卡死。所以标准做法是解码线程只负责拿 fd 出来放到一个无锁队列里RGA 消费线程从队列里拿 fd做缩放格式转换再交给下游。我用的队列结构很简单基于std::deque加互斥锁注意入队和出队时对 fd 的生命周期做好管理#include opencv2/opencv.hpp #include im2d.h #include rga.h #include rga_allocator.h int rga_process(int src_fd, int src_w, int src_h, int src_hor_stride, int dst_w, int dst_h) { int dst_size dst_w * dst_h * 3; // 输出 RGB int dst_fd allocate_dma_buffer(dst_size); rga_buffer_t src wrapbuffer_fd_t(src_fd, src_w, src_h, src_hor_stride, src_h, RK_FORMAT_YCbCr_420_SP); rga_buffer_t dst wrapbuffer_fd_t(dst_fd, dst_w, dst_h, dst_w, dst_h, RK_FORMAT_RGB_888); im_rect src_rect {0, 0, src_w, src_h}; im_rect dst_rect {0, 0, dst_w, dst_h}; // 同时做缩放格式转换颜色空间转换 int ret imresize(src, dst, src_rect, dst_rect, IM_SYNC); if (ret ! IM_STATUS_SUCCESS) { printf(RGA imresize failed\n); close(dst_fd); return -1; } // 这里可以直接给 NPU 或者 cv::Mat cv::Mat rgb_img(dst_h, dst_w, CV_8UC3); uint8_t *map_ptr (uint8_t *)mmap(nullptr, dst_size, PROT_READ|PROT_WRITE, MAP_SHARED, dst_fd, 0); memcpy(rgb_img.data, map_ptr, dst_size); munmap(map_ptr, dst_size); close(dst_fd); // 后续处理检测、编码、显示等 return 0; }这段代码里RK_FORMAT_YCbCr_420_SP对应 NV12 的 Y 平面加 UV 交错平面这是 MPP 硬解输出的标准格式。输出我选择了RK_FORMAT_RGB_888直接变成 3 通道 RGB方便后续送入 RKNN NPU 或 OpenCV。wrapbuffer_fd_t的完整参数签名在不同版本的 librga 里略有差别老版本可能不带 stride 参数如果编译不过换成两步先wrapbuffer_fd_t(fd, w, h, format)再单独调set_buf或者查一查你们版本的头文件。4.3 缓冲分配用 dma_heap 还是 dma_buf我建议 dma_heap在实际项目中我用了两种分配方式做对比drm方式libdrm的drmPrimeToFd需要管理 DRM 设备的打开和关闭。dma_heap方式打开/dev/dma_heap/system调用DMA_HEAP_IOCTL_ALLOC拿到 fd。结果我推荐dma_heap原因是它更简单直接不需要和 DRM 设备扯关系而且在 RK3588 上 CMA 分配响应很快。示例代码#include linux/dma-heap.h #include sys/ioctl.h #include fcntl.h #include unistd.h int allocate_dma_buffer(size_t size) { int heap_fd open(/dev/dma_heap/system, O_RDWR); if (heap_fd 0) { perror(open dma_heap failed); return -1; } struct dma_heap_allocation_data data; memset(data, 0, sizeof(data)); data.len size; data.fd_flags O_CLOEXEC | O_RDWR; int ret ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, data); close(heap_fd); if (ret 0) { perror(dma_heap alloc failed); return -1; } return data.fd; }分配完成后data.fd就是一个 dma-buf fd可以在进程间传递也可以直接用mmap映射到用户态做 CPU 读。但注意在零拷贝主链路上我们尽量不让 CPU 去 mmap 读否则前面省下来的带宽又会被硬生生抢回去。我上面代码里 mmap memcpy 只是为了演示最终转成 OpenCV Mat 的场景如果你的下游是 NPU直接把 fd 交给 RKNN 即可。4.4 完整的解码RGA工作流把前面几块拼起来一个完整的多路帧处理循环是网络输入 / 文件输入 ↓ MPP 解码输出 dma-buf fd stride 信息 ↓ 无锁队列 / 线程间消息传递 ↓ RGA imresize / imcvtcolor缩放 NV12-RGB ↓ NPU 推理 / 编码 / 显示 / 本地保存每路视频有一个解码线程和一个 RGA 消费线程RGA 消费线程完成之后立即把 fd 归还给 buffer 池复用避免反复分配释放造成性能抖动。我实际工程里用了一个简单的FrameBufferPool预先分配 4~6 组输出 fd用 atomic 计数器做取还实测效率比每帧动态分配高不少。5. 多路视频并行处理与性能优化实践5.1 多路实例的调度策略别让 RGA 排队饿死做多路处理最影响体验的是“路数”增加后的流畅度是否线性保持。单个 RGA 实例在 1080P 缩放上能跑到 600 FPS看着绰绰有余但一旦多路同时发起请求如果大家都是同步等待IM_SYNCRGA 硬件会变成串行队列整体吞吐反而下降。我的做法是统一走异步 轮询完成状态// 每路都保留一个 rga 请求上下文 struct RgaJob { int src_fd; int dst_fd; im_handle handle; // RGA 异步操作的句柄 }; int rga_process_async(RgaJob* job) { rga_buffer_t src wrapbuffer_fd_t(job-src_fd, src_w, src_h, src_hor_stride, src_h, RK_FORMAT_YCbCr_420_SP); rga_buffer_t dst wrapbuffer_fd_t(job-dst_fd, dst_w, dst_h, dst_w, dst_h, RK_FORMAT_RGB_888); im_rect src_rect {0, 0, src_w, src_h}; im_rect dst_rect {0, 0, dst_w, dst_h}; // 异步提交不会阻塞当前线程 job-handle imresize(src, dst, src_rect, dst_rect, IM_ASYNC); return 0; } int rga_wait_job(RgaJob* job) { return imsync(job-handle); // 等待完成 }这样每个线程提交之后可以继续去拉下一帧由单独的轮询线程统一imsync等待结果。听起来复杂但收益明显8 路同时提交每路都能维持在 30FPS 左右没有因为串行等待导致前面卡后面空。另外RK3588 有两个 RGA 核心如果想榨干性能可以按路数拆分到不同 RGA 上。librga 对双核心的自动负载均衡做了一些优化但在多线程场景下我自己实测手动指定设备节点比如让 0~3 路用 rga04~7 路用 rga1会更稳定可控。5.2 帧率与带宽评估这些数据到底怎么测的性能优化不能靠感觉我建议在工程里加一个统计模块统计每路的解码耗时、RGA 耗时、总耗时和 CPU 占用。我测的一组数据8 路 1080P H.26430FPS 输入如下项目数值纯 MPP 硬解单路 1080P 平均耗时约 2~3 msRGA 1080P→640x640 NV12→RGB 耗时约 1.5~2 ms8 路时 CPU 总占用率4 核左右16%~22%单路端到端延迟解码RGA队列约 8~12 ms开启双 RGA 后 8 路总吞吐约 250 FPS这里面分辨率缩小到 640x640 时RGA 的耗时主要用在格式转换上而不完全是缩放因为像素填充率高达 1920×1080。如果想继续降 CPU可以把输出也改成 NV12解码同格式这样 RGA 不需要做 RGB 转换耗时能降到 1ms 以下如果下游是 NPU通常也接受 NV12 或 BGR可以按需调整。注意数据会因固件、驱动版本、内存频率不同而浮动。RK3588 的内存带宽在双通道 LPDDR4/LPDDR5 下非常充裕但如果你的板子用的 DDR4 单通道数据可能会差 20% 左右。5.3 内存布局与 cache 一致性一个容易忽略的坑零拷贝一个字面上很美好但实际中有个对手叫“cache 一致性”。当 RGA 硬件写入了dst_fdCPU 再mmap读这个 dma-buf 时如果 CPU cache 里面有旧数据读到的就是过期的内容。我在第一次做 “RGA 输出后直接 cv::Mat” 时就出现过画面花屏和颜色不对的问题。解决方式有两种用DMA_BUF_IOCTL_SYNC做DMA_BUF_SYNC_START/DMA_BUF_SYNC_END#include linux/dma-buf.h int sync_dma_buf(int fd, bool start) { struct dma_buf_sync sync_info; memset(sync_info, 0, sizeof(sync_info)); if (start) { sync_info.flags DMA_BUF_SYNC_READ | DMA_BUF_SYNC_START; } else { sync_info.flags DMA_BUF_SYNC_READ | DMA_BUF_SYNC_END; } return ioctl(fd, DMA_BUF_IOCTL_SYNC, sync_info); }分配 buffer 时用DMA_HEAP_IOCTL_ALLOC的O_CLOEXEC标志然后调用mmap时指定MAP_SHARED配合DMA_BUF_SYNC来做 cache 维护。如果你的下游是 RKNN 的 fd 输入NPU 内部对 dma-buf 的 cache 操作是自洽管理的不需要你额外做 sync但只要有**“硬件写、CPU 读”**或“CPU 写、硬件读”的跨界就必须做 sync。这是零拷贝开发中非常容易出现花屏、颜色错乱、偶发撕裂的根源。6. 常见问题与排查实录6.1 解码卡死或长时间不返回表现decode_put_packet之后decode_get_frame一直出不来帧程序像卡住。排查思路确认输入的数据是不是完整的 ES 流有些容器格式MP4 里的 H.264 extradata需要提前解析出 SPS/PPS。检查是否设置过MPP_DEC_SET_EXT_BUF_GROUP如果 buf group 没有正确初始化为 DRM 类型内部分配可能走走停停。用mpi-control(mpp_ctx, MPP_DEC_SET_INFO_CHANGE_READY, ...)来应对分辨率变化但前提是帧宽高信息发生变化时要重置解码器状态。我遇到最多的其实是 SPS/PPS 问题。网络流RTSP虽然经常自带 SPS/PPS但在断线重连、流中间切换码率时MPP 需要重新解析。解决方法是在送入数据包之前先解析 SPS/PPS 并用MppDecCfg配置正确。6.2 RGA 输出花屏或绿屏这个问题九成出在stride 不匹配或format 不匹配上。绿屏通常意味着 UV 平面解出来的数据是乱序或缺失的花屏多伴随尺寸不对比如用 width 替代了 hor_stride。另外一个低频但顽固的问题是RGA 处理 NV12 时如果地址没有 16 字节对齐个别固件版本会出现边缘花屏。解决方式是分配 buffer 时把大小向上对齐到 256 字节并且确保 fd 的物理地址本身对齐。6.3 多线程下 CPU 升高但吞吐没涨我遇到过一次解码线程和 RGA 线程都在跑但 CPU 占用老高吞吐不涨最后发现是无锁队列打满了或者锁竞争太严重。换成boost::lockfree::spsc_queue之后CPU 立马降下来。还有一个隐形问题是频繁的mmap/munmap每帧都去 map 一次再 unmap会产生大量页表和 TLB 的开销。正确做法是固定分配几块 buffer复用它们而不是每帧重新分配销毁。6.4 RGA 设备节点打不开或 ioctl 失败通常是因为用户权限不够需要 root 或者加入 video 用户组。加入 tty/video 组sudo usermod -aG video $USER另外确认/dev/rga是否被其他进程独占虽然 RGA 驱动支持多开但某些老版本 BSP 可能有问题。6.5 和 RKNN 接不上NPU 报 buffer 错误RKNN 输入如果使用input_attrs.type RKNN_TENSOR_FMT_RGBA8之类必须保证 RGA 输出格式和 RKNN 期望格式一致否则驱动会返回参数错误。还有一个容易忽略的点是NPU 输入尺寸宽高对齐RKNN 通常要求宽度 16 对齐、高度 2 对齐RGA 输出的 stride 也要对齐到这个值。7. 性能瓶颈定位方法与调参工具7.1 用 perf 和自家计时器定位瓶颈排查性能问题时我一般先用perf top看 CPU 占用在哪一个函数sudo perf top如果看到memcpy或copy_user_enhanced_fast_string在顶部那基本可以断定你的链路里有隐式拷贝顺着源码去搜memcpy就好。如果看到rga_...或者ioctl那就是 RGA 太忙或调用次数太频繁。接着我在代码里插一个高精度计时器用std::chrono::steady_clock就行把每个阶段的时间戳打出来。这样能准确看出解码耗时多少、RGA 排队耗时多少、里面有没有隐藏的锁等待。7.2 RGA 参数调优先测像素格式再看尺寸RGA 的性能和输入输出格式强相关。同样是 NV12 转 RGB如果你的业务不需要输出 RGB888其实很多模型可以直接吃 YUV 输入例如 RKNN 的RKNN_TENSOR_NHWC或者 YOLO 系列的色域输出这时候可以让 RGA 只做缩放不做颜色转换性能直接翻倍。另外一个细节是RGA 的IM_SYNC模式在启用了异步模式之后每次imsync等待的对象如果还没提交完成会白白等待好几个毫秒。所以异步模式下要控制水位线——不要让队列里积压超过 4 个作业否则延迟会飙升。7.3 用 ion/dma_heap 分配策略影响性能RK3588 的systemdma_heap 分配的内存通常是从系统 CMA 里划出来的如果 CMA 区域分得太少高负载时会因为分配失败导致丢帧。我建议在设备树里预留足够 CMAlinux,cma { size 0x10000000; // 256MB };注意预留过大也会挤压普通内存所以数值要按业务情况折中。8 路 1080P 解码时所有解码帧的 buffer几十 MB RGA 输出 buffer 网络缓冲256MB 属于安全线如果你还做 8K 编码或 RKNN 大模型可能要留 512MB。8. 写在最后的经验之谈RK3588 的 MPP RGA 这套组合真正跑通后收益是立竿见影的。我印象最深的一次是把一个项目的视频前处理耗时从原来的 80ms 降到 6msCPU 占用从 90% 降到 20% 出头整机温度也从 82℃ 降到 66℃。这种体验会让人对“硬件加速”四个字产生强烈的信任感。我个人建议刚开始做这类项目的朋友一定不要一上来就冲向“多路 异步 极简锁”这些高级玩法。先把单路跑通把 fd 链路打通把 stride 和各种格式差异摸清楚再开多线程和多路。因为这些问题一旦并发起来排查复杂度是成倍增长的而在单路阶段定位和验证都很快。最后分享一个小技巧保存好你验证过的每种格式和尺寸组合的 RGA 耗时数据。不同分辨率、不同格式之间的性能差异有时候不是直觉能判断的比如 NV12 转 RGB888 和 NV12 转 BGR888看起来只差一个通道顺序但某些 RGA 版本里实现路径完全不同耗时会差 30% 以上。把这些数据攒下来后续做方案选型时能省下很多时间。