RK3588多路视频零拷贝处理:MPP+RGA链路设计与实践
2026/9/4 16:00:55 网站建设 项目流程

最近在做 RK3588 平台的多路视频处理,说实话,这个芯片的硬解码能力和 2D 加速能力如果不好好利用,性能会被传统方案拖垮一大截。我这边项目最终实现的效果是:8 路 1080p@30fps 的 H.264/H.265 视频流实时解码、缩放和格式转换,CPU 占用率控制在 10% 以内,内存带宽压力也远小于软解方案。这篇文章把 MPP + RGA 做零拷贝链路的关键设计和实操细节完整拆开讲,踩过的坑也都写出来了,给正在折腾 RK3588 视频处理的同行一个直接可参考的方案。

网上聊 RK3588 视频解码的文章不少,但大多数停留在“能解码”层面,很少有人把 MPP 和 RGA 之间的 buffer 流转讲清楚。这个项目的核心目标很明确:解码后不做任何 CPU 拷贝,直接让 RGA 接手处理,再送到显示或 AI 前处理环节。这套链路跑通之后,收益非常直观——CPU 占用从软解时的 70%-90% 直接降到 5%-10%,多路视频同时还挂 yolov8 推理都完全不虚。

适合看这篇文章的人主要是这几类:正在 RK3588 / RK3568 上做多路视频监控或边缘计算盒子的嵌入式工程师,被“解码帧怎么高效地送到显示、编码、AI 前处理”折磨过的开发者,以及想真正搞懂 MPP buffer 和 RGA 零拷贝机制的硬件爱好者。

1. 为什么多路视频处理非要折腾零拷贝

1.1 常规“解码→拷贝→处理”链路到底慢在哪

很多开发者第一次在 RK3588 上做视频处理,第一反应是用 FFmpeg 软解,或者直接读 MPP 解出来的帧再 memcpy 到新 buffer。这样做不是不行,但一旦路数上来,问题就全暴露了。

先说软解。FFmpeg 的 h264/h265 软解在 RK3588 的 Cortex-A76 核心上性能确实不错,单路 1080p 解码大概占用一个中核的 40%-60%。但 4 路、8 路一叠加,CPU 直接打满,留给业务逻辑、AI 推理、网络传输的算力就所剩无几了。更麻烦的是,软解出来的数据还是按解码帧格式排列,后续要做缩放、NV12 转 BGR、旋转镜像,全部要在 CPU 上再跑一遍,内存带宽和缓存都被打爆。

再说硬解但带拷贝的方式。很多人用 MPP 解码之后,拿到 MppBuffer,为了送给下游显示或做 AI,会先把数据 memcpy 到 CPU 内存里。一次 1080p NV12 帧的数据量是 1920×1080×1.5 ≈ 3MB,8 路 30fps 就是 720MB/s 的纯拷贝开销,而且还多了一次 CPU 参与和内存总线占用。如果送到 RGA 做转换,又要再拷一次,等于同样一份视频数据在内存里搬了两次甚至三次。

所以说,只要做过一版多路视频处理的人,都会本能地往“少拷贝”方向走。RK3588 的硬件架构本来就是为了这个目标设计的——MPP 解码输出的是 DMA buffer,RGA 可以直接操作 DMA buffer,DRM 显示也可以直接引用 DMA buffer,中间的搬运环节完全可以省掉。

1.2 MPP + RGA 零拷贝的整体设计思路

先想清楚一件事:在这个平台做零拷贝,到底“零”的是什么?是 CPU 不参与数据搬运,数据始终以 DMA-BUF 的形态在硬件单元之间传递。

整体数据流是这样一个链路:

视频流 → MPP 硬解码 → DRM/DMA-BUF fd → RGA 2D 处理(缩放/格式转换) → 显示 or AI 推理 ↑ ↓ CPU 只做控制流 直接消费

CPU 在这个链路里的角色只是“发指令”和“查状态”,真正的像素搬运由 MPP 和 RGA 各自内部的 DMA 引擎完成。视频帧从解码器出来之后,它的物理内存句柄可以通过文件描述符在整个系统里传递,谁需要谁就引用,不需要把数据复制一份。

这套设计的好处有三个方面。性能上,CPU 负载大幅降低,内存带宽让位给 GPU/NPU/DMA 这些专用单元;延迟上,拷贝少了,解码到显示的端到端时间更短;架构上,每个环节都是模块化的,解码、前处理、显示、推理都能独立替换。

当然零拷贝不是银弹。它引入的复杂度主要是 buffer 生命周期管理、跨模块句柄传递、硬件对齐要求。这些恰恰是实际开发中踩坑最多的地方,后面详细讲。

2. 硬件能力盘点:RK3588 上哪些资源在做功

2.1 MPP 解码器的真实规格

RK3588 内置的 VPU 解码能力在同级别 SoC 里属于天花板级别。H.264 和 H.265/HEVC 最高支持到 8K@30fps 解码,H.265 甚至可以到 8K@60fps(不同版本固件略有差异)。多路解码能力也很强,官方文档里给出的是 2 路 4K@60fps 或者 8 路 1080p@30fps 的典型配置。

MPP(Media Process Platform)是 Rockchip 提供的统一多媒体处理库,它屏蔽了 VPU 驱动的细节,向上提供 MppCtx、MppPacket、MppFrame、MppBuffer 这些抽象概念。这里要特别说明一下,MPP 解码器输出 buffer 的默认类型是 MPP_BUFFER_TYPE_DRM,背后挂在 DRM 设备上,分配的是连续的物理内存,天然具备被其它硬件模块直接引用的能力。

我实测下来,MPP 解码 8 路 1080p@30fps 时,CPU 占用率大概在 5%-8% 之间,主要是线程调度和 packet 投递的开销。VPU 核心占用和码率、分辨率相关,一般跑不到满载,还有余量做编码或其他任务。

2.2 RGA 2D 加速器的能力边界

RGA(Raster Graphic Acceleration)是 Rockchip 的 2D 硬件加速引擎。RK3588 上带的是 RGA2 和 RGA3 两代核心,其中 RGA3 性能更强,支持更高的分辨率和更多格式。RGA 能干的事包括:图像缩放、裁剪、旋转、镜像、格式转换(NV12、NV21、RGB888、RGBA8888、YUV420 等)、颜色空间转换、混合叠加。

对视频处理链路来说,RGA 承担的角色是“前处理加速器”:解码出来的 NV12 帧如果直接送屏,DRM 本身支持 NV12 显示,但通常需要把分辨率调到屏幕大小、裁剪 ROI,或者给 AI 推理模型做 letterbox、归一化、转 RGB,这些操作全部可以交给 RGA 一次性完成。

需要注意,RGA 不是万能的。它做不了复杂的 3D 变换和像素级算法(比如模糊、边缘检测这类需要卷积的操作),那些还是得上 GPU 或者 NPU。但在视频转码、分辨率适配这个场景里,RGA 的效率和带宽优势是非常明显的。

2.3 DRM buffer 是零拷贝的地基

要实现零拷贝,最关键的一点是统一 buffer 的类型。在 Linux 平台上,跨硬件模块传 buffer 的事实标准就是DRM(Direct Rendering Manager)的 DMA-BUF 机制

MPP 解码器分配出来的 MppBuffer,其底层就是一个 DRM buffer,有对应的 file descriptor(fd)。RGA 通过 librga 的 importbuffer 接口拿到这个 fd,然后直接在原物理内存上做 2D 操作。DRM/KMS 则可以直接用这个 fd 做 framebuffer 显示。这就跑通了整条零拷贝链。

用 fd 传递 buffer 带来的好处是:fd 是进程级的句柄,天然支持多进程共享(可以通过 SCM_RIGHTS 传给其他进程)。在单进程多线程模型里,只要保证引用计数正确,随拿随用、用完即还,非常方便。

3. 实操:从 RTSP 拉流到硬解码输出的完整链路

3.1 初始化 MPP 解码器

先说 SDK 环境。我用的系统是 RK3588 上的 Ubuntu 20.04(rockchip 官方 BSP),MPP 版本是 1.4.x,librga 版本是 1.9.0。用 rockchip-mpp 和 rockchip-librga 这两个仓库的源码编译即可,最好在板子上直接编译,避免交叉编译版本不一致的坑。

初始化 MPP 的流程很固定:

MppCtx ctx = NULL; MppApi *mpi = NULL; mpp_create(&ctx, &mpi); MPP_RET ret = mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC); if (ret != MPP_OK) { // 错误处理 }

这里要注意编码类型参数:H.264 对应 MPP_VIDEO_CodingAVC,H.265/HEVC 对应 MPP_VIDEO_CodingHEVC。如果解码流是动态变化的,比如视频源一会是 H.264 一会是 H.265,需要在检测到码流 SPS/PPS 变化时重新设置编码类型。

解码器初始化的关键参数有这几个:

MppDecCfg cfg; mpp_dec_cfg_init(&cfg); mpp_dec_cfg_set_u32(cfg, "base:grp_retry_times", 8); mpp_dec_cfg_set_s32(cfg, "base:timeout", 0); mpp_dec_cfg_set_s32(cfg, "base:frame_num", 4); // 预分配解码帧数量 mpp_dec_cfg_set_u32(cfg, "base:split_parse", 1); // 支持分片解析 mpi->control(ctx, MPP_DEC_SET_CFG, cfg);

frame_num这个参数很关键。它决定了 MPP 为解码器预分配多少个输出 buffer。如果设得太小,解码器会在帧率波动时频繁分配新 buffer,导致性能抖动;如果设得太大,内存占用会上去。8 路 1080p 场景下,我建议每路设 4-6 个帧 buffer,8 路加起来内存占用约 100MB 左右,可以接受。

3.2 正确配置解码参数

解码器配置里有一个容易忽略但影响很大的参数:split_parse。这个开关控制 MPP 是否对输入码流做分片解析。如果输入是 RTSP 拉流得到的 RTP 包,通常一个 packet 里包含的可能是半个帧或几个帧,split_parse 打开后 MPP 会自动做帧边界检测和切分,省去了开发者在用户态做 H.264 Annex-B 格式转换的工作。

对于从文件读入的码流,还需要注意 packet 的时间戳和 EOS 标识。MPP 的解码流程是异步的,put(发送 packet)和 get(获取 frame)各走各的队列,正确姿势是循环做“发送一个 packet → 尝试取帧”,而不是发送完所有 packet 再取帧。

贴一段实际可用的解码循环骨架:

while (!eos) { // 1. 读取码流到 packet_buf size_t read_size = fread(packet_buf, 1, packet_size, fp); if (read_size <= 0) { eos = 1; break; } // 2. 用 MppBuffer 包装输入数据 mpp_packet_init_with_buffer(&packet, input_buffer); mpp_packet_set_size(packet, read_size); mpp_packet_set_eos(packet, 0); // 3. 发送给解码器 mpi->decode_put_packet(ctx, packet); // 4. 取回解码帧 do { ret = mpi->decode_get_frame(ctx, &frame); if (ret == MPP_OK && frame) { // 处理一帧解码结果,详见 3.3 process_decoded_frame(frame); mpp_frame_deinit(&frame); } else { break; // 当前没有可取帧 } } while (1); mpp_packet_deinit(&packet); }

这个循环写得比较朴素,但已经把核心流程点出来了。实际项目里需要加线程:一个线程负责拉流和 put packet,一个或多个线程负责 get frame 和处理,这样可以避免解码带宽波动导致某一端卡住。

3.3 拿到解码帧后如何“不拷贝”地交给 RGA

解码拿到 MppFrame 之后,真正关键的操作来了——如何把它变成 RGA 能用的输入。

这里分两步走:第一步,从 MppFrame 里取出底层的 fd;第二步,把这个 fd 导入 RGA 的 buffer 结构。

第一步的代码:

MppBuffer mpp_buf = mpp_frame_get_buffer(frame); int fd = mpp_buffer_get_fd(mpp_buf);

拿到 fd 之后,第二步交给 RGA。RGA 的 im2d API 提供了一个叫importbuffer_fd的函数,可以把 fd 包装成rga_buffer_t

rga_buffer_t src_buf; src_buf = importbuffer_fd(fd, &src_info);

这里的src_info是一个rga_buffer_info_t结构,需要手动填充当前帧的宽高、格式、stride 等参数。特别注意,宽高不能用解码帧的显示分辨率直接填,要查一下 MppFrame 的实际 buffer 大小

rga_buffer_info_t src_info = {0}; src_info.width = mpp_frame_get_width(frame); src_info.height = mpp_frame_get_height(frame); src_info.format = RK_FORMAT_YCbCr_420_SP; // NV12 src_info.fd = fd;

MPP 解码出来的输出格式默认是 NV12(如果是 10bit 码流,可能输出 NV15/P010,那就要在初始化时强制指定输出格式)。RGA 对 NV12 的RK_FORMAT_YCbCr_420_SP是原生支持的,速度最快。

到这里,数据还没发生过任何一次 CPU 拷贝。fd 从 MPP 手里交到 RGA 手里,物理内存从头到尾是同一块,只是换了个“所有权标记”。这就是零拷贝的核心。

4. RGA 2D 加速实操:缩放、裁剪、格式转换一网打尽

4.1 配置 RGA 输入输出

RGA 处理一帧图像,需要配置输入 buffer、输出 buffer 和对应的图像信息。我用的是 librga 的 im2d API,相比老的 rga1 接口,im2d 更简洁,而且支持 RGA2/RGA3 自动调度。

先初始化 RGA 设备:

rga_open();

然后是核心的 resize + 格式转换调用。下面这个例子完成的是:将解码得到的 1920x1080 NV12 帧缩放成 640x640 RGB888,方便直接喂给 yolov8 之类的模型:

rga_buffer_t src_buf = importbuffer_fd(src_fd, &src_info); rga_buffer_t dst_buf = importbuffer_fd(dst_fd, &dst_info); im_rect src_rect = {0, 0, 1920, 1080}; // 源图像裁剪区域 im_rect dst_rect = {0, 0, 640, 640}; // 目标图像区域 imresize(src_buf, dst_buf, src_rect, dst_rect, 0);

这里有几个容易踩的细节:

第一个是src_rect的宽高。如果解码输出带裁边(比如 HEVC 的关键帧),MPP 给的 width/height 可能是对齐过的,实际有效区域由mpp_frame_get_width/height结合mpp_frame_get_ver_crop这些字段确定。处理不对就会出现画面偏移或者绿边。保险做法是先用 RGA 的 crop 能力裁剪出有效区域,再做后续处理。

第二个是dst_buf对应的 buffer 要从哪里来。如果你只是做显示缩放,输出 buffer 可以是直接通过 DRM 分配的 scanout buffer;如果是喂给 AI 模型,则可以分配一块 CPU/GPU 都能访问的连续内存。这里推荐继续走 DMA-BUF,不要退回 malloc 的普通内存,否则 RGA 还得做一次导入,且导入普通内存的性能比分给 DMA buffer 差得多。

4.2 关键对齐参数与常见格式坑

RGA 对宽度有对齐要求。不同的 RGA 版本对齐粒度不一样,RGA2 要求宽度 16 像素对齐,RGA3 要求宽度 64 像素对齐。如果源图宽度不是对齐的,RGA 会默认填充尾部像素,但你输出时要搞清楚 padding 的实际宽度。

MPP 解码输出帧的宽度是自动对齐的,通常 1080 这种分辨率不太会有问题,但 720 这种宽度是 16 的倍数,也没问题。真正容易出问题的场景是自定义分辨率,比如 1922 宽度或者 540 这种奇数宽度。遇到这类情况,要做的是先把有效区域裁出来,让 RGA 处理的数据宽度对齐,避免在 RGA 输出端得到奇怪的行尾数据。

格式转换的坑我列一下,都是我实际踩过的:

  • NV12 和 NV21 不要搞混。NV12 是 U 和 V 平面交替存储时先 U 后 V,NV21 反一下。RGA 也有对应的 RK_FORMAT_YCbCr_420_SP 和 RK_FORMAT_YCrCb_420_SP,填错了画面偏色,而且偏色的规律还不是简单的色相偏移,调试起来很恶心。
  • RGB 和 BGR 的顺序。RK3588 的 NPU 通常要求 RGB 输入,但 OpenCV 和很多开源代码默认 BGR,RGA 转换时一定要确认目标格式。我在项目里吃过一次亏,模型输出结果精度发飘,查了半天竟然是输入图像通道顺序反了。
  • stride 参数。RGA 的 buffer 除了宽高,还有 stride(一行的字节数),如果 source 和 destination 的实际 stride 和系统默认计算的不一致,需要在rga_buffer_info_t里显式设置,否则会出现斜纹、错位。
  • 色彩空间。默认情况下 RGA 做 NV12→RGB 按照 BT.601 limited range 转换。如果你解码的是 HDR 或 BT.709 内容的流,颜色会发灰。要指定色彩空间可以用imcvtcolor或者在improcess接口里传 color space 参数。

4.3 RGA 多路并发与性能控制

当路数达到 8 路时,RGA 并发处理就要讲究策略了。RGA2 和 RGA3 是独立的硬件单元,可以并行。librga 内部通过任务队列管理并发请求,但我在多线程环境中发现,如果你开 8 个线程同时调用 imresize,librga 内部需要做同步,锁竞争明显。

我的做法是:开一个单独的 RGA 处理线程池,线程数等于 RGA 核心数(RK3588 上通常是 2 个),把每一帧的处理任务通过队列分发。这样既避免了多线程同时调 librga 的锁开销,又能把 RGA2 和 RGA3 都用上。

RGA 处理一帧 1080p 缩放到 640x640 的速度非常快,实测单次调用在 1ms 左右。8 路同时处理也就 8ms 的硬件占用时间,每帧 33ms 的周期内完全够用。

5. 性能测试与对比:软解 vs 零拷贝硬解链路

5.1 测试环境与方法

测试平台是 RK3588 开发板,8GB LPDDR4x,系统 Ubuntu 20.04,内核 5.10。测试视频源是 8 路 1080p@30fps 的 H.264 码流,平均码率 4Mbps。对比方案有两个:FFmpeg 软解 + swscale 缩放;MPP 硬解 + 零拷贝 RGA 处理。

测量指标包括:CPU 总占用率(top/pidstat),内存占用(/proc/meminfo),以及单帧处理延迟(在解码输出打时间戳)。

5.2 实测数据对比

方案CPU 占用率内存占用(仅视频缓冲)单帧处理延迟稳定性
FFmpeg 软解 + swscale85% - 95%约 300MB18ms - 35ms掉帧明显
MPP 硬解 + 零拷贝 RGA7% - 11%约 120MB5ms - 8ms稳定 30fps

这个数据差异是意料之中的。软解时 CPU 全部在解码线程里做运动补偿、反变换、环路滤波,连处理缩放的算力都被挤占,导致延迟高、帧率不稳定。而硬解 + 零拷贝方案把像素层面的活全部交给专用硬件,CPU 只做控制逻辑,自然游刃有余。

再说一个关键细节:内存带宽。软解方案中 swscale 处理一帧 1080p NV12→RGB 需要读一遍写一遍,约 6MB 的内存流量,8 路就是 48MB/帧,30fps 下约 1.44GB/s。零拷贝方案中 RGA 同样做格式转换,但它是 DMA 直读直写,虽然也需要内存带宽,但不经过 CPU 缓存,对 CPU 侧的性能影响几乎为零。实测跑 8 路时,CPU 侧的内存带宽占用从软解方案的 30% 降到了 5% 以下。

6. 常见问题与排查实录

6.1 解码相关坑

MPP 长时间运行后不取帧、丢帧。这个问题的根源通常是 buffer 回收不及时。MPP 解码器的 buffer pool 是有限的,如果 get_frame 拿到的帧没有被及时释放(mpp_frame_deinit),buffer 池枯竭,解码就会停摆。排查方法是看mpi->decode_get_frame的返回值,长时间返回MPP_ERR_BUFFER_FULL就说明下游处理速度跟不上。

解码花屏或卡在 I 帧。RTSP 拉流场景很常见,因为 RTP 分包可能导致 SPS/PPS 丢失。解决方法是解码器初始化时开启MPP_DEC_SET_ENABLE_DEINTERLACE?不对,是开启split_parse,同时在上层做关键帧请求(比如 RTSP 的 PLAY 请求带Require: precondition或主动请求关键帧)。更稳妥的做法是保存最近一个 SPS/PPS,发现解码器返回MPP_STATUS_STREAM_ERR时重新注入。

8 路解码串流或画面串台。这是典型的 context 隔离问题。MPP 虽然是线程安全的,但一个 MppCtx 只能被一路码流使用。如果你图省事共享 ctx,后果就是 VPU 解码状态机被搞乱。正确的做法是每一路视频流创建独立的 MppCtx,互不干扰。

6.2 RGA 相关坑

RGA 报错E_RGA_ILLEGAL_PARAMETER。这个错误九成是 width/height/stride 不对齐。检查一下源和目标的宽高是否是 16 的倍数,检查rga_buffer_info_t里是否填了正确的 stride,检查 fd 对应的 buffer 是否真的可读(用 lspci 查一下分配内存区域?不,用drmPrimeFDToHandle验证一下 fd 是否有效)。

画面整体偏移但颜色正常。这个问题的典型原因是源 buffer 的 stride 比实际宽度大,RGA 按 stride 读到数据,但你裁剪区域用的是实际宽度,导致每一行都偏移。解决方法是把src_rect的 width 设置成实际宽度,并且在src_info里显式设置wstride

RGA 转换后画面发白或发绿。颜色空间转换的锅。先确认格式编号,NV12 用RK_FORMAT_YCbCr_420_SP,如果视频源是 P010 10bit,要用RK_FORMAT_YCbCr_420_SP_10B。再确认色彩范围,HDMI/SDI 过来的视频常是 full range,但 RGA 默认 limited range,需要用imcvtcolor手动转换。

6.3 零拷贝链路的低级错误

fd 传递后忘记 dup,导致资源被提前释放。这是一个经典问题。当你把 fd 作为消息传给另一个线程或进程时,如果接收方只是在内部存了一个整数,而没有调用dup(fd)抬高引用计数,发送方一旦关闭 fd,接收方手里的 fd 就成了野指针。轻则 RGA 报错,重则直接段错误。我的统一规则是:谁使用谁负责持有,跨线程传 fd 必经 dup,使用完再 close

用 CPU 指针访问 DMA buffer 导致 cache 不一致。零拷贝链路下开发者很容易忍不住用 mmap 把 DRM buffer 映射到用户态,然后去读像素、打点调试。注意,这个操作在 DMA buffer 上是可行的,但它会破坏“数据不过 CPU”的设计初衷,而且如果不做 cache 同步,你读出来的可能是旧数据。如果确实要调试,用drmCommandWrite?不对,用drmPrimeFDToHandle+drmMap映射用户态,看完马上 unmap,并且使用drmHandleEvent或者显式调用 cache flush 接口(例如drmModeAddFB2之外的 sync ioctl)。

6.4 排查工具建议

排查 MPP 和 RGA 问题时,用好这三个工具能省不少时间:

  • v4l2-ctl:查看 VPU 节点的状态,确认是否真的在硬解。
  • drm_info:查看 DRM 设备的 framebuffer 和 plane 信息,确认显示链路是否正常。
  • rga_info:librga 自带的一个测试工具,可以单独测试 RGA 的缩放、转换能力,排查 RGA 本身是否工作正常。

另外强烈建议开 MPP 的 debug log。设置环境变量MPP_DBG_TYPE_LEVELMPP_DBG_LEVEL_DEBUG,能看到每个 packet 的输入输出信息,定位码流错误和解码异常非常有用。

7. 后续可以这么扩展

这套 MPP + RGA 零拷贝链路搭好之后,往下的扩展空间很大。最常见的是接 RK3588 的 NPU 做 AI 分析,RGA 缩放转好的 RGB 数据可以直接通过 DMA-BUF 送给 RKNN 的输入。也可以接 MPP 编码器,做多路转码——解码出来的 DRM buffer 直接作为编码器输入,整个转码过程完全不碰 CPU 数据搬运。

我自己现在的版本是把这条链路接上了 yolov8 推理和 RTSP 输出,一套流程跑下来,8 路视频同时做检测再编码输出,整机 CPU 都压得很低。后面打算再进一步,用 DRM/KMS 的 overlay plane 把多路解码画面直接送显示,省掉一个合成图层。

最后说个我踩过三次的细节:DMA-BUF 的生命周期管理一定要统一收口。我在代码里专门封装了一个VideoBuffer类,内部维护 fd、引用计数、映射地址,所有模块只允许通过这个类获取 buffer 信息,绝不允许直接暴力传裸 fd。这三个月跑下来,从来没有出现过 buffer 泄漏或野指针。代码设计的价值在后期的稳定性上体现得淋漓尽致。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询