1. 为什么RKMPP值得单独拿出来讲
搞瑞芯微平台音视频开发的兄弟,大概率都绕不开一个东西——MPP。全称Rockchip Media Process Platform,是瑞芯微官方提供的一套硬件编解码抽象层。你在RK3588、RK3568、RV1106这些芯片上做视频编解码,不管上层用的是FFmpeg、GStreamer还是自己写的RTSP服务,底层十有八九跑的都是它。
但MPP这个东西有个特点:官方文档不算少,但散。交叉编译的说明在SDK的buildroot里,API的说明在doc目录下,性能调优的经验基本靠社区帖子和自己踩坑。我第一次在RK3568上跑MPP的时候,光是把库编出来、让demo跑通,就折腾了整整两天。后来在RK3588上做多路解码,又遇到了码流分配、buffer复用、RGA配合这一堆问题。
这篇东西是我自己从交叉编译到性能优化的一整套实战记录。适合两类人看:一类是刚拿到瑞芯微开发板、想把MPP跑起来但不知道从哪下手的;另一类是把MPP跑通了、但发现帧率上不去、CPU占用高、多路跑不稳,想进一步压榨硬件性能的。我会把每一步为什么这么做讲清楚,包括参数怎么算、坑在哪、怎么绕。
2. 先把RKMPP的定位搞清楚
2.1 MPP在瑞芯微软件栈里的位置
很多人一开始会搞混MPP和FFmpeg的关系。简单说,FFmpeg是通用的多媒体框架,MPP是瑞芯微自己的硬件编解码接口。FFmpeg在瑞芯微平台上可以通过h264_rkmpp、hevc_rkmpp这类解码器调用MPP,也可以完全不用FFmpeg,直接调MPP的C API。
MPP往下对接的是VPU(视频处理单元)和RGA(2D图形加速器)。VPU负责H.264/H.265/VP8/VP9这些格式的硬编硬解,RGA负责缩放、旋转、格式转换、合成。MPP把这两者的调用封装成统一的MppCtx、MppApi接口,你创建解码器的时候指定编码类型,送码流进去,它吐YUV或RGB出来。
这里有个关键点:MPP本身不做封装格式解析。你给它的是裸码流(annexb或length-prefixed),不是MP4文件。所以实际项目里通常是FFmpeg负责demux,MPP负责decode,RGA负责后处理,最后送显示或编码。这个分工要在一开始就想清楚,不然后面架构会乱。
2.2 什么场景下必须用MPP
如果你只是做个简单的视频播放,用系统自带的播放器或者FFmpeg软解也能跑。但以下几种情况,MPP基本是唯一选择:
- 多路高清解码:RK3588号称能跑32路1080p解码,这个只有走VPU硬解才可能,软解CPU直接爆。
- 低延迟编码:做RTSP推流、视频会议、车载DVR,编码延迟要求几十毫秒级别,软编根本达不到。
- CPU资源紧张:RV1106这种单核A7的小芯片,软解720p都费劲,必须硬解。
- 需要和RGA配合做图像处理:比如多路视频拼接、AI推理前的预处理,RGA的零拷贝通路只有和MPP配合才能发挥。
反过来说,如果你只是偶尔解个视频、对功耗不敏感、平台又是x86,那没必要折腾MPP。选型阶段想清楚,能省很多事。
2.3 版本选择与SDK获取
瑞芯微的MPP是开源的,仓库在GitHub上(rockchip-linux/mpp)。但要注意,不同芯片、不同SDK版本对应的MPP分支可能不一样。我的建议是:优先用你手上SDK里自带的MPP源码,而不是直接clone最新master。原因很简单,SDK里的版本是经过瑞芯微验证的,和内核驱动、VPU固件匹配。你随便换个版本,可能出现ioctl不兼容、固件加载失败这类问题。
如果你用的是Buildroot或Yocto,MPP通常已经作为package存在了,直接make mpp就行。但很多时候我们需要自己交叉编译,比如要把MPP集成到一个已有的Qt工程里,或者SDK里没有预编译。下面重点讲手动交叉编译。
3. 交叉编译:从工具链到产物验证
3.1 工具链的选择与验证
交叉编译第一步永远是确认工具链。瑞芯微平台基本都是ARM架构,RK3588是Cortex-A76+A55(aarch64),RK3568是A55(aarch64),RV1106是Cortex-A7(armv7)。所以你要先确认目标架构,再选对应的工具链。
我常用的是Linaro的GCC或者SDK自带的arm-rockchip830-linux-uclibcgnueabihf这类。验证工具链是否可用,跑一条命令:
aarch64-linux-gnu-gcc -v看输出的target是不是aarch64-linux-gnu。如果是,说明工具链本身没问题。接下来要确认sysroot,也就是目标平台的根文件系统头文件和库。交叉编译MPP需要依赖一些系统库,比如libdrm、libion(旧版本)、pthread等。这些库的路径必须指向目标平台的sysroot,不能指向你主机的/usr/include,否则编出来的东西链接的是x86的库,跑不起来。
提示:很多人交叉编译失败,90%是sysroot没设对。用
--sysroot=/path/to/target/rootfs显式指定,比依赖环境变量靠谱。
3.2 CMake交叉编译配置实战
MPP用的是CMake构建系统。交叉编译的核心是写一个toolchain file。我一般命名为rk3588.toolchain.cmake,内容大概这样:
set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(TOOLCHAIN_PATH /opt/toolchain/gcc-aarch64-linux-gnu) set(CMAKE_C_COMPILER ${TOOLCHAIN_PATH}/bin/aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PATH}/bin/aarch64-linux-gnu-g++) set(CMAKE_SYSROOT /opt/target/rootfs) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)这里几个点解释一下。CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER的意思是,找可执行程序(比如cmake自己调用的工具)时不要在sysroot里找,因为sysroot里没有主机能跑的程序。LIBRARY ONLY和INCLUDE ONLY则是强制只在sysroot里找库和头文件,避免误用主机的。
然后配置编译选项:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=rk3588.toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DHAVE_DRM=ON \ -DRKPLATFORM=ON \ -DHAVE_AFBC=ONHAVE_DRM打开DRM支持,用于直接显示;RKPLATFORM是瑞芯微平台特有的一些优化;HAVE_AFBC是压缩帧缓冲支持,RK3588上建议开,能省带宽。
3.3 编译产物与部署验证
编译完成后,产物在build/目录下,主要是librockchip_mpp.so和一堆测试程序(mpi_dec_test、mpi_enc_test等)。把这些拷到板子上,注意库的搜索路径。可以放到/usr/lib,或者设置LD_LIBRARY_PATH。
验证第一步,跑mpi_dec_test解一个H.264文件:
./mpi_dec_test -i test.h264 -t 7 -n 100 -o out.yuv-t 7表示H.264,-n 100表示解100帧。如果能看到帧率输出、out.yuv大小合理,说明MPP基本跑通了。这一步跑不通,后面性能优化都是空谈。
注意:如果报
mpp_dev_init失败,大概率是内核里的VPU驱动没加载,或者设备节点/dev/mpp_service权限不对。先ls /dev/mpp_service确认存在,再检查权限。
4. 核心API与数据流:把MPP用对
4.1 解码流程的五个关键步骤
MPP的API看起来多,但解码流程其实就五步:初始化、配置、送码流、取帧、释放。我用伪代码串一遍:
// 1. 创建上下文 MppCtx ctx; MppApi *mpi; mpp_create(&ctx, &mpi); // 2. 初始化解码器,指定编码类型 mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC); // 3. 配置解码参数 MppDecCfg cfg; mpp_dec_cfg_init(&cfg); mpp_dec_cfg_set_u32(cfg, "base:split_parse", 1); mpp_dec_cfg_set_u32(cfg, "base:need_split", 1); mpp_dec_cfg_set_u32(cfg, "base:disable_error", 0); mpp_dec_cfg_set_u32(cfg, "base:timeout", 0); mpi->control(ctx, MPP_DEC_SET_CFG, cfg); // 4. 循环送码流、取帧 while (has_data) { MppPacket packet; mpp_packet_init(&packet, data, size); mpi->decode_put_packet(ctx, packet); MppFrame frame; while (mpi->decode_get_frame(ctx, &frame) == MPP_OK) { // 处理frame,比如送RGA或显示 mpp_frame_deinit(&frame); } mpp_packet_deinit(&packet); } // 5. 释放 mpp_destroy(ctx);这里面有几个参数值得展开。split_parse和need_split是控制码流解析方式的。如果你的输入是完整的一帧一帧(比如从FFmpeg demux出来的AVPacket),设成1让MPP自己拆;如果输入已经是单帧,可以设0减少开销。disable_error设0表示遇到错误不直接退出,而是尝试恢复,做流媒体的时候这个很重要,网络丢包导致的码流错误不能让整个解码器挂掉。
4.2 Buffer管理与零拷贝
MPP性能优化的核心之一就是减少内存拷贝。默认情况下,MPP解码出来的帧在内部buffer里,你decode_get_frame拿到的是引用。如果你要送RGA处理,可以直接把MppFrame的fd传给RGA,实现零拷贝。
关键API是mpp_frame_get_fd和mpp_frame_get_buffer。RGA的im2d接口支持直接传fd:
MppBuffer buffer = mpp_frame_get_buffer(frame); int fd = mpp_buffer_get_fd(buffer); // 把fd传给RGA rga_buffer_t src = wrapbuffer_fd(fd, width, height, RK_FORMAT_YCbCr_420_SP);这样数据全程在DMA buffer里流转,CPU只负责控制,不碰数据。实测在RK3588上,4路1080p解码+RGA缩放,走零拷贝比走memcpy,CPU占用能从60%降到25%左右。
提示:零拷贝的前提是MPP的buffer group配置正确。创建解码器时要设置
MPP_DEC_SET_EXT_BUF_GROUP,让MPP从你指定的group里分配buffer,这样fd才是稳定的、可共享的。
4.3 编码端的配置要点
编码流程和解码类似,但配置项更多。以H.264编码为例,关键参数有:
| 参数 | 说明 | 推荐值 |
|---|---|---|
| rc_mode | 码率控制模式 | VBR或CBR |
| bps_target | 目标码率 | 根据分辨率定 |
| fps_in_num/fps_in_denom | 输入帧率 | 30/1 |
| gop | 关键帧间隔 | 30-60 |
| quality | 编码质量 | 根据场景调 |
| profile | 编码档次 | high |
码率计算有个经验公式:bps = width * height * fps * factor。factor对于H.264一般在0.07到0.15之间。比如1080p30,1920108030*0.1 ≈ 6.2Mbps。这个只是起点,实际要根据画面复杂度调。
CBR适合直播推流,码率稳定;VBR适合本地录制,画质优先。RK3588的编码器支持到4K60,但多路的时候要注意VPU的总带宽限制。
5. 性能优化:从能跑到跑得好
5.1 多路解码的架构设计
单路解码跑通不难,难的是多路。RK3588标称32路1080p解码,但实际能跑多少路,取决于你的架构。我做过一个16路1080p30解码+AI推理的项目,总结下来几个关键点:
第一,解码线程和取帧线程要分开。MPP的decode_put_packet和decode_get_frame可以在不同线程调用,送码流是异步的,取帧是同步的。如果在一个线程里既送又取,容易因为取帧阻塞导致送码流不及时,VPU饿死。
第二,每路解码器独立一个MppCtx,但共享一个buffer group。这样内存分配统一管理,避免每路各自malloc导致碎片。
第三,用MPP_DEC_SET_PARSER_SPLIT_MODE让MPP内部多线程解析。RK3588有8个核,解析可以并行。
实测下来,16路1080p30,CPU占用大概40%,VPU占用70%左右。如果架构没设计好,比如所有路共用一个线程,可能8路就卡了。
5.2 RGA配合的带宽优化
RGA虽然快,但也不是免费的。每次RGA操作都要读写内存,带宽是瓶颈。RK3588的DDR带宽大概在10GB/s级别,4路1080p60的YUV420数据,每帧约3MB,每秒240帧,就是720MB/s的读写,来回就是1.4GB/s。如果再加上缩放、格式转换,带宽很快就吃紧。
优化手段有几个。一是尽量用RGA的原地操作,比如只做格式转换不做缩放,减少一次读写。二是用AFBC压缩,RK3588支持,能把带宽降一半。三是合并操作,比如缩放+格式转换+旋转,RGA可以一次完成,不要分多次调用。
注意:RGA的并发数有限,RK3588上有多个RGA核心,但每个核心同时只能处理一个任务。多路的时候要用RGA的任务队列,不要每个线程直接调,否则会互相阻塞。
5.3 内存与缓存调优
MPP的buffer分配策略对性能影响很大。默认用的是ion或dma-buf,具体看内核版本。RK3588新内核用dma-buf heap。分配的时候要注意对齐,VPU对buffer的地址对齐有要求,一般是16或32字节对齐。
另外,YUV数据的stride(行跨距)也很关键。如果stride和width不一致,RGA处理的时候要额外处理,影响效率。建议创建buffer的时候显式指定stride,让它和width对齐到64字节。
缓存方面,如果CPU要访问解码后的YUV数据(比如做AI推理前的预处理),要注意cache一致性。MPP出来的buffer默认是non-cacheable的,CPU读会慢。可以用mpp_buffer_sync做cache同步,或者让RGA直接处理,避免CPU碰。
6. 常见问题与排查实录
6.1 解码失败问题速查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| mpp_dev_init失败 | 驱动未加载/权限不足 | ls /dev/mpp_service,检查权限 |
| 解码花屏 | 码流不完整/stride不对 | 检查输入码流,确认stride |
| 解码卡住 | 送码流不足/取帧阻塞 | 检查线程模型,加超时 |
| 帧率低 | 单线程/拷贝过多 | 多线程+零拷贝 |
| 内存泄漏 | frame未deinit | 检查每帧是否释放 |
6.2 几个我踩过的坑
第一个坑:decode_get_frame返回的frame,如果不mpp_frame_deinit,buffer不会释放,跑一会儿就OOM。这个在demo里可能看不出来,因为demo跑完就退出了。实际长跑必须每帧释放。
第二个坑:多路的时候,如果所有路用同一个MppCtx,会串流。必须每路独立ctx。但buffer group可以共享。
第三个坑:RK3588的VPU固件版本要和MPP库版本匹配。有次我升级了内核但没升级MPP,结果解码直接失败,报固件加载错误。后来把MPP也升到对应版本才好。
第四个坑:RGA处理的时候,如果源和目标的格式不一致,比如源是NV12目标是RGB,RGA会自动转换,但性能会下降。如果可能,尽量让格式一致,转换放到最后一步。
6.3 性能瓶颈定位方法
性能上不去的时候,不要瞎猜,用工具定位。top看CPU,cat /sys/kernel/debug/mpp_service/status看VPU占用,perf看热点函数。如果VPU占用低但帧率上不去,说明是送码流或取帧的瓶颈,不是VPU的问题。如果VPU占用高但帧率还是低,说明码流复杂度太高或者VPU频率不够,可以试试调频。
RK3588的VPU频率可以通过/sys/class/devfreq调整,但一般不建议手动调,默认的DVFS策略已经够用。如果确实需要,可以看看是不是散热问题导致降频。
7. 一些实战心得
MPP这个东西,入门门槛不算高,但要用好需要理解整个数据流。我的建议是,先把单路跑通,把API的每个参数都试一遍,看看效果。然后再上多路,这时候架构设计比API调用更重要。
另外,瑞芯微的社区虽然不如一些大厂活跃,但GitHub上的issue和wiki还是有不少干货。遇到问题先搜issue,大概率有人踩过。实在不行,看源码,MPP的源码不算复杂,核心逻辑在mpp_dec.cpp和mpp_enc.cpp里,读一遍对理解行为很有帮助。
最后说个实际项目里的经验:不要过度优化。我见过有人为了省一点CPU,把架构搞得特别复杂,结果维护成本极高。先保证功能正确、稳定,再谈优化。MPP本身的性能已经很强了,大部分场景下,把零拷贝和多线程做好,就够用了。