做嵌入式Linux视频处理这几年,手上的项目几乎绕不开“采集-解码-处理-显示/编码”这条链路。大多数时候,我们都是在ARM SoC上跟CPU性能较劲,尤其到了多路视频解码加实时处理这种场景,CPU软解根本扛不住,硬解成了唯一出路。瑞芯微的RK3588在这类需求里算是个热门平台,自带强大的VPU(视频编解码单元)和RGA(2D图形加速单元),再加上MPP(Media Process Platform)这套统一媒体处理框架,理论上可以做到视频解码、格式转换、缩放、叠加全程不经过CPU拷贝。这篇就从头到尾聊一遍我在RK3588上做多路硬解码与2D加速的完整实践,重点放在MPP+RGA的零拷贝视频处理链路上。
我最初接到这个需求的时候,目标很明确:8路1080p视频流同时硬解码,解完之后每一路还要实时做YUV到RGB转换、缩放、OSD叠加,最终送给AI推理模块。如果按照传统思路,先软解再逐帧用CPU转格式,RK3588那颗八核CPU基本就被榨干了,AI推理就没资源跑。所以硬解码和2D加速是唯一合理的路线。
这篇内容适合正在做RK3588、RK3568这一类瑞芯微平台音视频开发的工程师,也适合准备把视频处理任务从CPU卸载到专用硬件上的人。我会把MPP硬解码的调用流程、RGA做2D加速时的格式约束和踩坑记录、以及零拷贝到底“零”在哪一环、怎么做到真零拷贝,全部拆开来讲,附上可直接参考的代码思路和排查方法。
1. 项目背景与整体方案选型思考
1.1 为什么选择MPP+RGA这套组合
瑞芯微平台上的MPP并不是一个新的软件库,它从RK3288时代就存在,一路迭代到RK3588已经很成熟了。MPP把VPU的能力封装成一套统一API,底层自动分配硬件资源,上层只需要关心码流输入和帧输出。而RGA是独立的2D硬件加速器,负责对解码后的视频帧做各种内存拷贝、格式转换、缩放、旋转、镜像、混合叠加操作,完全不占用CPU算力。
这两者放在一起用,最核心的原因只有一个:让视频数据在解码器和RGA之间流动时,全程不落入CPU可见的内存区域做数据搬运。解码器直接输出到一块DRM/ION物理连续内存,RGA直接以FD(文件描述符)方式读取这块物理内存做处理,处理完再输出给下一级,CPU始终只操作元数据(指针、时间戳、尺寸),真正的像素数据一次都没被CPU碰过,这就是“零拷贝”项目里最实在的价值。
如果不用这套组合,一般做法是:VPU解码到一块内存,然后mmap到用户态CPU拷贝一份做格式转换,再拷贝给推理输入。这条路在单路低分辨率场景下勉强能接受,但到了8路1080p就会立刻暴露问题——内存带宽占用高、CPU占用高、延迟抖动明显。RK3588的CPU确实不弱,但也没富裕到能扛8路视频处理的份上,所以方案敲定为“解码走MPP、格式转换和缩放走RGA、内存管理走DMA-BUF/ION”。
1.2 整条链路的基本流程与模块划分
按我在项目里的划分,整条视频处理链路可以拆成五个模块:拉流/解封装模块、MPP硬解码模块、RGA处理模块、Buffer池管理模块、输出/推理对接模块。
拉流模块负责RTSP/GB28181/本地文件取流,输出H.264/H.265裸流数据,这一层CPU消耗基本可以忽略。硬解码模块负责把裸流送进MPP解出原始视频帧(通常是NV12格式),解码器工作完全在VPU里,CPU只做控制。RGA处理模块拿到解码帧的DMA-BUF FD,做格式转换和缩放,最后扔给输出模块——可能是DRM/KMS直接显示,也可能是AI推理输入,还可能是硬编码器输入。整个链路里最关键的是Buffer池,它是零拷贝能否成立的基础,如果这块设计不好,前面解码再快也会被内存拷贝卡住。
我实际采用的Buffer管理思路是:启动时就向系统申请一个大的ION/DMA-BUF内存池,将内存块按帧大小+对齐需求划分好,解码器输出、RGA输入输出、显示/推理输入统一从这个池子里取内存,用引用计数管理生命周期。这样从解码到处理到消费,全程复用的都是同一批物理内存,最多只是引用计数在加加减减。
2. 硬件平台与系统环境准备
2.1 RK3588平台关键外设和能力
RK3588本身的数据手册大家都查得到,我只说和视频处理强相关的部分。它的VPU支持非常广泛,包括H.264/H.265/VP9/AV1的硬解码,其中H.265和VP9支持到8K@60fps级别,H.264支持到4K@120fps级别。这个解码能力意味着8路1080p对它来说远没到极限,我实测8路1080p解码时VPU占用大概在30%上下,大量余量留给RGA和AI。
RGA方面,RK3588有一颗RGA3和一颗RGA2,RGA3是新一代核心,性能和功能更强,支持最大8K输入,RGA2则负责一些基础任务。两套核心同时可用,意味着理论上可以把“格式转换”和“合成叠加”分流到不同核心上做并行处理,后续优化时可以考虑这一点。
内存方面,RK3588支持最多32GB LPDDR4/4X/5。需要提醒的是,解码器虽然可以正常申请内存,但要想性能稳定,建议在内核启动参数里预留足够的CMA空间,或者直接用DMA-BUF Heap来管理编解码内存。CMA预留太小会导致VPU在高分辨率或高帧率场景下内存申请失败,这个坑我踩过,后面细说。
2.2 系统与依赖库版本选择
我使用的是Rockchip官方提供的Linux SDK,内核版本5.10,系统是Debian11的rootfs。如果你拿到的是Android版本,思路也差不多,只是运行时服务和权限管理有差异。
MPP(librk_mpp)建议直接用SDK里带的版本,或者从Rockchip的GitHub仓库拉最新版自己编译。重点在于MPP版本要和你内核的VPU驱动版本匹配,否则可能出现解码能力协商不一致的情况。RGA库(librga)同样用SDK自带的,或者从rockchip-linux/rga拉代码编译,编译时注意选择USE_RGA3相关开关。
环境层面还要确认以下几点:/dev/dri/renderD128节点是否存在(用于DRM提交显示),/dev/rga节点是否存在(RGA设备节点),kernel config里是否打开了CONFIG_DMABUF_HEAPS、CONFIG_ROCKCHIP_ION等相关选项。调试时也可以挂载debugfs查看RGA和VPU的工作状态,比如/sys/kernel/debug/rkrga/load能看到RGA实时负载,/sys/kernel/debug/mpp_service下有编解码器工作状态。
3. MPP硬解码链路完整实操
3.1 MPP解码器的基本结构与初始化
使用MPP的核心思路是:创建一个解码上下文,绑定解码类型,配置解码参数,然后循环往解码器里塞压缩数据包(Packet)、从解码器里取解码帧(Frame)。MPP的常见解码类型包括H.264、H.265、VP9、AV1等,我们在项目里用的主要是H.264和H.265拉流。
初始化阶段需要特别注意两点:首先,创建context和初始化必须要绑定到一个解码线程上,MPP内部虽然可以多线程调用,但同一个context不建议跨线程同时操作,多路解码应该用多个context,每个context配一个独立线程;其次,MPP解码器对输入码流缓冲区有对齐要求,码流数据一般在送入前要做mpp_packet_init,Buffer大小要用MPP_ALIGN做对齐,不要无脑malloc一个原始长度就扔进来。
一个最简单的初始化流程是这样的:
MppCtx ctx; MppApi *mpi; mpp_create(&ctx, &mpi); MPP_RET ret = mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC); if (ret != MPP_OK) { printf("mpp_init failed\n"); return ret; } MppDecCfg cfg; mpp_dec_cfg_init(&cfg); // 输出格式首选NV12,零拷贝链路的默认选择 mpp_dec_cfg_set_u32(cfg, "type", MPP_VIDEO_CodingAVC); mpp_dec_cfg_set_u32(cfg, "width", 1920); mpp_dec_cfg_set_u32(cfg, "height", 1080); mpp_dec_cfg_set_u32(cfg, "format", MPP_FMT_NV12); mpi->control(ctx, MPP_DEC_SET_CFG, cfg);实际使用中,MPP是支持动态解析分辨率变化的,所以如果码流是可变分辨率,可以不写死宽高,让MPP自己通过SPS/PPS解析。但解码器输出格式建议固定在NV12或者NV12_10LE,毕竟NV12是RGA和显示最友好的格式之一。
3.2 解码主循环与帧输出管理
解码主循环的逻辑其实不难,难在控制好输入和输出的节奏。MPP的这种控制流模型,可以理解为“生产者-消费者”模型——主循环不断把裸流数据包塞进去,然后立刻检查有没有解好的帧取出来。但要注意MPP内部有解码延迟,表现为解码器在拿到几个packet之后才输出第一帧,这是正常现象,不是卡住。
解码主流程代码结构大致如下:
MppPacket packet; MppFrame frame; // 读取一包码流数据(从RTSP/文件等) size_t len = read_stream(data_buf, data_size, &pts); mpp_packet_init(&packet, data_buf, len); mpp_packet_set_pts(packet, pts); mpp_packet_set_eos(packet, 0); // 送包 ret = mpi->decode_put_packet(ctx, packet); if (ret == MPP_OK) { // 取帧 while (mpi->decode_get_frame(ctx, &frame) == MPP_OK) { if (frame) { process_frame(frame); // RGA处理 mpp_frame_deinit(&frame); } } }这里有一个很容易犯的错误:码流包从文件或者网络上读出来后,所占用的内存空间在整个decode_put_packet调用周期内必须保持有效,不能提前释放。MPP内部并不拷贝你的码流数据,它只是把内存地址记录下来交给VPU异步读取。你如果立刻释放了这块内存,轻则花屏、解码报错,重则直接崩溃。正确做法是维护一个码流Buffer回收队列,等MPP确认这一包处理完之后再复用这块内存。可以通过mpp_packet_get_eos和回调机制或者简单地在足够长时间后再回收来实现。
第二个容易踩的坑是EOS处理。当输入码流结束时,一定要先发送一个带EOS标志的空包,然后循环调decode_get_frame,直到拿到带EOS标志的最后一个帧,才能真正关闭解码器。否则VPU内部还有几帧缓存的画面没有刷出来,直接断开会导致最后几帧丢失或者状态错乱。
3.3 多路解码的线程模型与Buffer组管理
多路解码时,我一开始图省事,想在一个线程里循环处理所有路,结果发现一路码流出现网络抖动堵住了,后面所有路都跟着遭殃。这里还是老老实实采用“一路一线程”的模型,每路解码线程对应一个MPP context,线程只循环处理当前路的packet和frame,线程间通过队列传递解码输出的帧元数据。
但这里立刻会碰到内存分配问题:8路解码同时跑,如果每帧都现申请现释放,内存碎片和申请耗时都会成为瓶颈。所以我用MPP的MppBufferGroup来做统一管理。所有解码输出buffer都由这个buffer group分配,解码器会直接从group里复用空闲buffer,不会反复向内核申请内存。
MppBufferGroup group; mpp_buffer_group_get_internal(&group, MPP_BUFFER_TYPE_ION); mpp_buffer_group_limit_config(group, 1920 * 1080 * 3 / 2 * 16, 0); // 将group设置到解码器 mpi->control(ctx, MPP_DEC_SET_EXT_BUF_GROUP, group);上面这段代码里,limit_config的第二个参数是组内buffer总量限制,我按16帧解码缓冲来计算。对于1080p NV12来说,一帧约3MB,16帧就是48MB左右,8路就是接近400MB内存预留。这些内存在启动时一次性申请好,之后跑起来基本不再动态分配。
MPP_DEC_SET_EXT_BUF_GROUP是这套管理方式的开关,一旦设置成功,解码器输出的frame就会从这块group里取内存,配合后续的RGA处理,可以做到“解码输出的FD直接给RGA,全程不需要拷贝”。
4. RGA 2D加速处理实操
4.1 RGA的能力边界和API选择
RGA最擅长的操作包括图片缩放、格式转换、旋转、镜像、裁剪、Alpha叠加、色彩空间转换等。做视频处理时,用到最多的是“NV12转RGB(BGRA/RGBA)并缩放”这个组合操作,RGA一次调用就能完成,不需要两步走。
Rockchip官方推荐使用im2d这套API,它统一封装了RGA2/RGA3的差异,函数接口非常简洁。实际代码里主要用到这几个函数:imresize(缩放)、imcvtcolor(格式转换)、imrotate/imflip(旋转镜像)、improcess(一站式处理,可以同时做缩放、格式转换、裁剪等)、imcheck(参数检查)。如果你需要极致控制,也可以直接操作底层rga_ops接口,但日常开发用im2d就够了。
使用im2d之前,必须明确一点:RGA输入输出的内存必须是物理连续内存。所以上一章强调的ION/DMA-BUF内存池,在RGA环节仍然是核心。你可以直接把MPP解出来的MppBuffer通过mpp_buffer_get_fd拿到对应的FD,再传给RGA,根本不经过CPU视角的虚拟地址。
4.2 NV12转RGB并缩放的完整调用示例
下面这段代码演示了最核心的场景:把一帧1920x1080的NV12数据,转成640x640的RGB888,同时完成缩放,供AI推理输入使用。我用的是im2d的improcess接口,一步到位。
#include "im2d.h" #include "RgaApi.h" // 输入buffer来自MPP解码输出 int in_fd = mpp_buffer_get_fd(mpp_frame_buffer); // 输出buffer来自我们自己申请的RGA buffer pool int out_fd = rga_buffer_pool_get_fd(); rga_buffer_t src = wrapbuffer_fd_t(in_fd, 1920, 1080, RK_FORMAT_YCbCr_420_SP, 0, 0); rga_buffer_t dst = wrapbuffer_fd_t(out_fd, 640, 640, RK_FORMAT_RGB_888, 0, 0); im_rect src_rect = {0, 0, 1920, 1080}; im_rect dst_rect = {0, 0, 640, 640}; int ret = improcess(src, dst, src_rect, dst_rect, IM_SYNC); if (ret != IM_STATUS_SUCCESS) { printf("improcess failed: %d\n", ret); }wrapbuffer_fd_t的第一个参数是输入FD,后面依次是宽、高、像素格式。注意这里传入的宽高必须是RGA能接受的对齐值,NV12输入宽通常要求16像素对齐,高8像素对齐;RGB888输出宽度建议4像素对齐。1920和640天然满足,但如果你处理的是比如1282x722这种比较怪的分辨率,就需要在做buffer分配时就按对齐后的width和height去分配,否则RGA会返回参数错误。
improcess最后一个参数IM_SYNC表示同步调用,也就是说这个接口会等RGA硬件完成操作后才返回,返回后输出buffer里的数据保证可用。如果改成IM_ASYNC,则函数提交任务后立即返回,需要用imsync或imWait去等结果,性能更好但调试复杂度高,我自己习惯先在同步模式下把流程调通,再针对热点路径改成异步。
4.3 常见格式转换的注意点和对齐陷阱
RGA的格式支持非常全,但对齐要求非常严格,很多新手在这里花掉大量时间。
NV12/NV21这类半平面格式,输入宽度必须16像素对齐、高度8像素对齐;RGB888/RGB565宽度必须4像素对齐。这里说的不是图像真实宽度,而是RGA内部一次处理的最小粒度。如果你的图像宽度不满足要求,两种解决办法:一是分配buffer时就按对齐后宽度分配,图像数据的stride大于实际显示宽度,RGA通过设置wstride/hstride来正确解析图像;二是先用RGA做一次copy到对齐大小的buffer,再继续做后续操作。第一种方式效率高,推荐优先使用。
再强调一个很容易导致花屏的点:NV12和NV21的U/V分量顺序是不同的。NV12是Y平面+UV交错(U在前),NV21是Y平面+VU交错(V在前)。如果你解码器输出的是NV12,RGA输入配置却写成了NV21,出来的画面颜色会是偏色的,最常见的表现是红色和蓝色互换。排查这类问题最快的方法是先拿一张纯色测试图过一遍链路,看RGA输出的RGB数值对不对。
4.4 RGA处理多路视频时的调度与并行
当8路视频同时需要做RGA处理时,RGA设备的调度就很重要了。RK3588的RGA3和RGA2在/dev/rga节点下,同一时刻只能有一个任务在硬件层面执行,如果你8个线程同时往里面提交任务,底层驱动会排队,但多线程提交会带来上下文切换开销。
更推荐的做法是:写一个统一的RGA任务队列,由一个或两个专门线程来提交任务。RGA3和RGA2在最新驱动里可以通过im2d的参数选择核心,IM_RGA3和IM_RGA2可以分开指定。我在项目里把“格式转换类任务”全部划给RGA3,把“小尺寸缩放/OSD叠加”划给RGA2,两个核心可以并行工作,实测吞吐提升了接近50%。
队列调度时还有一点要注意:RGA任务之间是非抢占的,一个超大分辨率任务会把后面一堆小任务全部堵住。如果系统里同时有8K和1080p的任务混合,最好根据任务大小做优先级分组,把阻塞风险高的聚合任务放在最低优先级通道,保证小任务的实时性。
5. 零拷贝链路设计与Buffer管理实践
5.1 零拷贝到底“零”在哪,能带来多大收益
很多文章喜欢提“零拷贝”,但真正说清楚零在哪的很少。在MPP+RGA这条链路上,零拷贝是指:从VPU解码输出开始,到RGA处理完成,再到下一级消费(AI推理/显示/编码),视频帧的像素数据始终停留在同一块物理内存上,CPU不参与任何数据搬运。
传统做法中,CPU需要做数据搬运的地方包括:VPU输出拷贝到用户态(读FD)、用户态格式转换(走CPU计算)、转换完再拷贝给推理输入(写FD)。三次全算下来,1080p NV12一帧约3MB,8路30fps就是每秒720MB的纯搬运量,还要加上格式转换的CPU计算。这个量级会让CPU持续高负载,同时消耗大量内存带宽。零拷贝之后,CPU只负责传递FD和元数据,解出来的帧在哪里、处理完的帧还在哪里,CPU全程只是“指挥”,不亲自“搬砖”。
我在同一块板子上对比过,非零拷贝方案下8路1080p@30fps解码+RGB转换,CPU占用约75%,AI推理基本跑不动;换成MPP+RGA零拷贝链路后,同样8路CPU占用降到12%以下,AI推理可以全速运行。这就是零拷贝最直接的收益。
5.2 DMA-BUF FD在解码器与RGA之间的流转
实现零拷贝的关键技术点,在于FD的流转。MPP解码输出的MppBuffer背后是ION/dma-buf,通过mpp_buffer_get_fd拿到FD后,这个FD可以直接作为RGA的输入内存描述符。RGA驱动内部会根据FD找到对应的物理内存并完成DMA操作,整个过程不经过用户态地址空间映射,也不需要CPU去读这些像素。
这里有个细节需要维护清楚:FD的生命周期。如果你在RGA任务还没完成时就释放了MppBuffer,RGA硬件可能正在读这块内存,轻则花屏,重则崩溃。建议的做法是:给MppBuffer加引用计数,解码输出一帧时引用计数+1,RGA任务提交时+1,RGA任务完成回调时-1,推理消费时+1,推理完成后-1,引用计数归零时才真正归还buffer给MPP复用。
// 伪代码:引用计数维护 MppFrame frame = get_frame_from_mpp(); buf_ref_inc(frame); // 交给RGA前加锁 int fd = mpp_buffer_get_fd(mpp_frame_buffer); submit_rga_task(fd, ...); // RGA回调中 buf_ref_dec(frame); // RGA处理完解锁你可以用一个简易的atomic变量记录引用数,不需要依赖复杂框架。需要注意的是,mpp_frame_deinit调用时MPP本身会尝试回收buffer,所以在引用计数不为零时不能轻易deinit,要等所有消费者都释放完再统一归还。
5.3 Buffer池的设计:容量计算与复用策略
Buffer池的容量需要精心计算,太大浪费内存,太小会导致解码器阻塞。我在设计8路1080p解码+处理池时,按下面这个公式估的量:
每路缓冲帧数 = 解码器内部缓冲(3~5帧) + RGA处理队列缓冲(2~3帧) + 推理输入缓冲(2帧) + 显示/编码缓冲(2帧)总共大约10~12帧每路。按1080p NV12单帧3MB计算,8路就是240MB~288MB。RK3588搭配8GB以上内存时这个预留完全可以接受。如果内存紧张,可以对低优先级路的缓冲帧数做压缩,比如显示/编码缓冲减到1帧,但解码器内部缓冲不建议低于4帧,否则高码率或I帧周期不规律时容易卡顿。
Buffer复用策略上,我采用了“空闲队列+使用中计数”的模式。初始所有buffer挂在空闲队列,需要时从空闲队列取,处理完成后回到空闲队列。如果空闲队列为空,就等——绝对不能临时申请内存,临时申请大概率会触发缺页中断或CMA压缩,导致帧周期抖动,这在视频链路上是致命的。
5.4 多路Buffer调度的同步与防撕裂
多路视频并行时,每一路的解码帧处理完成时间是不固定的,可能出现某一路特别快、某一路特别慢的情况。Buffer调度层要做的,是把每一路独立的处理节奏汇总成统一的心跳,保证所有路的帧不会在输出端错乱。
我这里的做法是:每路解码线程把自己处理完的帧写入一个环形队列,消费端(推理线程或者显示线程)按帧时间戳从各队列里取帧。最关键的一步是设置一个同步基准时间戳,逻辑上每一路都输出“同一个时间点”附近的帧,避免出现一路已经走到第100帧、另一路还在第80帧这种越拉越大的情况。做法是在启动各路线程后,先让所有路由到一个公共起点(比如第一帧到达后同时开跑),之后每消费一帧都检查与基准的时间差,如果某路落后太多,就主动丢帧追赶进度,保证整体流水的节奏一致。
6. 性能实测数据与问题排查经验总结
6.1 实测性能数据:多路解码与2D加速占用
我手上这套环境是RK3588+8GB LPDDR4X,CPU为默认调频策略,内存频率2133MHz。测试输入是8路1080p@30fps的H.264码流,总码率约24Mbps。解码由MPP硬解,每路RGA做NV12转RGB888并缩放到640x640,最终送给一个轻量级AI模型做检测。
实测数据可以给你做个参照:
| 配置 | VPU占用 | CPU占用 | RGA占用 | 帧率(每路) | 端到端延迟 |
|---|---|---|---|---|---|
| 单路1080p解码+RGA+AI | 4% | 5% | 8% | 30fps | 约40ms |
| 8路1080p解码+RGA+AI | 28% | 11% | 42% | 30fps | 约50ms |
| 8路1080p解码+CPU软件转RGB | 28% | 78% | 0% | 22fps | 约120ms |
CPU占用从78%降到11%,性能差距一目了然。RGA占用42%说明它还有余量,后续如果加OSD叠加或者多路缩放,也不会立刻触顶。端到端延迟从解码到AI输出约50ms,对于监控类场景完全够用,如果要进一步压延迟,可以考虑把RGA从同步模式改成异步模式,再对VPU侧做低延迟配置。
6.2 排查实录:解码输出花屏与RGA颜色异常
做这套系统时,我先后踩过两个特别典型的坑,排查过程可以分享一下。
第一个坑:解码出来的画面偶尔花屏,现象是画面顶部的几条线出现颜色错乱。一开始以为码流有问题,查了很久才发现问题出在buffer复用上。我码流包内存复用过快,MPP还没异步读完就被重新写入新数据,导致VPU拿到的是半新半旧的数据。解决方式是给码流包加一个“已使用”标记,确认MPP处理完当前packet后才把这块内存放回空闲队列。
第二个坑:RGA输出的RGB图像颜色偏色,红色和蓝色互换。检查后发现输入格式我写成了RK_FORMAT_YCbCr_420_SP,但实际上MPP输出的是NV12,RK_FORMAT_YCbCr_420_SP默认解释顺序是Y+UV,如果我的输入实际是NV21(Y+VU),就会偏色。最后通过纯红色测试图验证了数据送进去的U/V顺序,改成匹配的实际格式就好了。这里建议大家在调试RGA颜色问题时,可以直接喂一张代码生成的纯色NV12图,看输出哪种颜色反了,判断是U/V顺序问题还是色彩空间矩阵选择问题。
6.3 性能分析工具与瓶颈定位方法
定位性能瓶颈时,单纯靠printf打点效率太低。我的习惯是先看硬件占用率,再逐级排查。RK3588上可以直接读这几个节点:
# RGA负载 cat /sys/kernel/debug/rkrga/load # VPU工作状态 cat /sys/kernel/debug/mpp_service/summary # CPU各核负载 mpstat -P ALL 1RGA的load节点会显示每个核心的忙碌时间占比,如果RGA3一直冲到90%以上,说明该往RGA2分流或者压缩单帧处理时间。VPU的summary能看到当前活跃的编解码通道和帧率,确认解码能力是否有富余。CPU负载如果异常偏高,优先看是不是你在转换格式的代码里偷偷用了CPU计算——这种情况经常发生在你“只是把指针传了一下,底层却隐式copy”的时候。
6.4 避免过度优化的几点心得
最后分享几段个人经验,不一定是最优解,但对避坑很有帮助。
第一,不要一开始就追求异步和极致性能。先用同步模式把整条链路跑通,确保每一帧数据是正确的,再逐步改成异步模式,优化性能。我在初期一上来就用异步模式调RGA,结果数据竞争导致各种偶发花屏,排查成本非常高。后来改成同步模式确认逻辑正确,再改回异步,问题瞬间明朗。
第二,不要忽略内存对齐和stride问题。很多偶发问题不是因为代码逻辑错,而是因为某个输入宽度不符合RGA对齐要求,导致边界像素处理失败。写RGA处理函数时,第一件事就是用imcheck检查参数,确认源和目标的宽高、格式、stride全部合法,再提交硬件。
第三,多路视频线程要克制使用锁。锁的竞争会造成线程阻塞,阻塞会让buffer队列积压,积压又会拉长延迟。我在代码里尽量用无锁队列(SPSC环形队列)配合原子变量做引用计数,只有在极端情况下才用互斥锁保护全局状态。
RK3588这套MPP+RGA的组合,做多路视频处理确实是一套非常顺手的方案。它最大的价值不是某个孤立的硬件加速能力,而是把解码、处理、显示、推理的内存链路统一到DMA-BUF/FD这套体系里,让整条视频流水线的数据流动成本降到最低。如果你正准备在自己的项目里做类似的多路视频处理,希望这篇文章能帮你少走一段弯路。