ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RK3588多路视频监控实战:VPU硬解+RGA转换+GPU渲染全解析

RK3588多路视频监控实战:VPU硬解+RGA转换+GPU渲染全解析 1. 项目概述与方案选型为什么盯上RK3588做多路监控手头这个项目其实来得挺直接——客户要求做一台能同时接入16路1080p摄像头的边缘视频监控终端画面要流畅、延迟要低最好还能在前面板实时预览后续再加AI分析。这种需求放到三五年前基本就是x86工控机加显卡的路子但现在国产化替代的呼声越来越高功耗、成本、体积又卡得死所以第一眼就把目光锁定在瑞芯微的RK3588上。RK3588这颗片子在这个场景里可以说是“天生对口”。它最大的吸引力在于异构算力布局8核CPU4×A764×A55、Mali-G610 MP4 GPU、独立的VPU视频编解码单元、6TOPS NPU还有专门做图像转换的RGA模块。多路视频监控系统最吃紧的三个环节——多路解码、图像格式转换、多路渲染显示正好分别对应VPU、RGA和GPU这三个硬件单元。这意味着16路1080p的H.264/H.265码流解出来CPU基本可以处于“躺平”状态主核资源可以留给业务逻辑或者后续的AI分析。我当时的选型思路很简单先看硬解能力是否能覆盖多路数需求再看GPU渲染能否保证多路预览不卡顿最后确认整个软件链路在Linux/BSP层面是否成熟。RK3588的VPU解码能力官方标称是同时解码32路1080p30fps的H.264/H.265实际测下来16路留足余量GPU方面Mali-G610支持Vulkan/OpenGL ES 3.2画多路视频纹理绰绰有余软件层面Rockchip提供了MPPMedia Process Platform、RGA库和DRM/KMS显示栈的完整示例。这三个条件都满足方案基本就定了。这个项目的价值不只在于“能跑通”更在于整条链路如何把硬解码、格式转换、GPU渲染这三件事无缝串联起来。很多人以为接上摄像头、调个播放器就能多路预览实际一上手就会发现解码缓冲管理、帧同步、纹理上传、显示时序任何一个环节处理不好都会造成画面撕裂、内存暴增、延迟飙升。下面我会从整体架构讲起把关键模块拆开讲透最后给出一套可落地的实现路线和排坑经验。2. 整体流水线设计解码、转换、渲染三单元如何协同2.1 系统架构的四大层次监控终端从功能上可以拆成四层接入层、解码层、处理层、显示层。接入层负责从RTSP拉流本项目统一走RTSP摄像头海康/大华/宇视都有兼容性优先解码层调用Rockchip MPP库把H.264/H.265码流通过VPU硬件解码成NV12格式的YUV帧处理层做两件事一是通过RGA把NV12转成RGBA8888二是把转换后的帧上传到GPU纹理显示层通过DRM/KMS或者EGLFS方式把纹理贴到屏幕上配合GPU渲染完成多路画面的布局与叠加。这个分层最大的好处是每一层都有明确的硬件单元承接互不争抢。我最开始试过用FFmpeg的软解方案16路跑起来CPU立刻被吃满而且内存拷贝频繁延迟也压不下来。换成MPP硬解后CPU占用率直接降到个位数整个系统的瓶颈才从“能不能解”变成“怎么把解出来的帧高效送显”。这里有一个容易被忽略的设计决策YUV转RGBA为什么不用GPU而用RGA因为Mali-G610虽然能做格式转换但纹理上传还得先把YUV数据拷贝到GPU可访问的内存再做一次shader转换中间多一次DMA映射和一次绘制开销。RGA是Rockchip专门做旋转、缩放、格式转换的硬件模块NV12转RGBA8888的吞吐量非常高而且走的是物理连续内存转完直接就能被GPU引用省掉中间拷贝。实测下来RGA单次转换1080p耗时大约1.2ms左右GPU shader方案要4~5ms差距非常明显。2.2 内存流转与缓冲区管理有了硬件单元的分工接下来最关键的就是内存怎么在VPU、RGA、GPU之间高效流转。这个项目里我统一用的是DRM的dma-buf机制。VPU解码输出的帧是NV12格式的dma-bufRGA读取这个dma-buf并输出RGBA8888格式的新dma-buf然后GPU通过EGL扩展EGL_EXT_image_dma_buf_import把这个dma-buf直接导入为纹理。整个过程零拷贝不走CPU。零拷贝说起来简单调起来全是坑。首先是内存类型要选对推荐用Rockchip的ion或dma-heap分配物理连续内存并且要保证VPU、RGA、GPU三者的地址访问权限都放通。其次dma-buf的同步要处理好VPU写完一帧、RGA读到一半、GPU正在纹理化这几步之间的fence同步搞错就会出现花屏。我是用DRM的syncobj同步对象来做fence每个环节处理完就signal一下下游等fence再操作这样虽然代码复杂一点但能彻底避免数据竞争。对于缓冲区数量我采用的是“解码双缓冲显示三缓冲”的组合。解码端每个通道保留2个解码帧缓冲一个被VPU写入一个被RGA读取显示端每个通道3个纹理缓冲轮流做“上传中、待显示、显示中”三个状态。这里如果缓冲太少会出现解码等待太多则会拉高内存延迟16路1080p的NV12单帧大约3.1MBRGBA单帧8.3MB再乘以通道数和缓冲数内存开销一下就会上到几百MB所以必须精打细算。提示RK3588的NPU和GPU共用一部分系统带宽多个硬件单元同时跑时容易抢带宽。内存频率建议在DTS里锁定到最高档我实测锁频后GPU和VPU同时满载时的帧率稳定性提升约15%。2.3 线程模型与任务调度多路监控场景天然是“多路并行”线程模型如果设计不好很容易出现某一路卡顿拖垮全局。我采用的方案是“每通道独立解码线程统一渲染线程”。每个通道一个pthread专门跑MPP解码和RGA转换解出来的RGBA帧放入无锁环形队列渲染线程统一从16个队列里取帧按布局把纹理绘制到屏幕上。为什么渲染线程只用一个因为DRM/KMS的提交操作是全局串行的如果多个线程同时去做页面翻转page flip要么加锁等待要么乱序显示。统一渲染线程反而更简单——它只需要快速轮询所有队列有新的帧就更新对应纹理然后一次性提交合成。实测16路1080p场景下渲染线程每帧耗时约8ms还留有余量。解码线程之间的优先级要区分开。我用SCHED_FIFO把解码线程设为实时优先级确保VPU任务不被其他后台任务抢占渲染线程用普通优先级但绑定到大核CPU4-7。另外还有一点RK3588有大小核架构解码线程如果跑在小核上可能导致dma-buf分配延迟高大核上就顺畅很多。我通过在启动时设置CPU亲和性把16个解码线程均匀分布到4个大核上实测解码整体吞吐提升了约10%。3. 硬解模块深入解析MPP库使用与VPU真正实力3.1 MPP接口调用要点Rockchip的MPP库是在Linux平台上做视频编解码的标准SDK接口设计跟其他平台略有不同用起来需要先理解它的会话模型。整体流程是初始化MPP会话、绑定解码类型、创建解码分组decoder group、循环喂入码流数据、取解码输出、处理完成后释放。我贴一段核心初始化代码方便对照理解#include rk_mpi.h MppCtx ctx; MppApi *mpi; MppDecCfg cfg; // 创建解码上下文 mpp_create(ctx, mpi); // 设置解码类型为H.264 MppCtxType type MPP_CTX_DEC; MppCodingType coding MPP_VIDEO_CodingAVC; mpi-control(ctx, MPP_CTX_SET_CODEC_TYPE, type); mpi-control(ctx, MPP_CTX_SET_CODEC_TYPE, coding); // 配置解码参数 mpp_dec_cfg_init(cfg); mpp_dec_cfg_set_u32(cfg, base:grp, 16); mpp_dec_cfg_set_u32(cfg, base:split_parse, 1); mpi-control(ctx, MPP_CMD_SET_DEC_CFG, cfg); // 初始化解码器 mpp_init(ctx, MPP_CTX_DEC, coding);这里有一个很重要的参数split_parse它表示让MPP内部自动拆解码流里的NALU。开启后你只需要按RTSP包过来的数据顺序喂给MPP它自己会处理分包和重组。如果不开启就需要自己解析H.264的StartCode工作量会增加不少所以建议默认开启。解码分组group这个参数也别忽略。MPP支持把多个解码实例放到一个组里统一调度设置合理的分组大小有助于让VPU在解码多路码流时更高效地复用硬件资源避免频繁地在不同编码格式之间切换上下文。3.2 解码输出与帧获取解码输出的帧通过mpp_frame结构体描述里面包含宽高、格式、pts、dma-buf fd等信息。从MPP获取一帧的标准动作是调用mpi-dequeue(ctx, frame)拿到帧后用DMA_BUF_IOCTL_SYNC做一个读同步然后就可以交给RGA做格式转换了。这里有个实操细节MPP在解码过程中可能不会立刻输出完整帧特别是遇到B帧较多的码流。设计缓冲区时要考虑到“解码延迟”建议在启动阶段先喂入一定量的码流再开始取帧避免渲染端拿到空帧。我一般会在每个通道内部维护一个25帧左右的缓冲窗口等解码器输出稳定后再让渲染线程介入。取完帧后必须调用mpi-enqueue(ctx, frame)把帧缓冲还给解码器复用。很多初学者会漏掉这一步结果解码队列被耗尽画面很快就卡死。我自己的习惯是解码线程里dequeue/enqueue成对出现每取一帧用完立刻归还这样能保证解码缓冲始终处于循环复用状态。3.3 VPU并发能力的实测评估官方说RK3588的VPU最多支持32路1080p30fps解码我在项目里跑了16路H.265 Main Profile的实况流实测数据如下指标数值总解码帧率480 fps16路×30fpsVPU占用率约65%单路解码耗时2.8ms~3.5ms解码错误帧率0.1%CPU占用解码相关不足5%从这个数据能看出来VPU确实余量很足。当然这只是H.265的情况如果换H.264解码效率会更高一些因为H.264的宏块解析开销更小。真正制约多路数的反而不是VPU本身而是后端的RGA转换和GPU渲染是否跟得上。我做过一个压力测试把解码路数加到24路VPU占用大概爬到85%仍然稳定但RGA转换出现了排队单路转换时间从1.2ms涨到2.5ms最终画面预览开始出现微小的延迟累积。这验证了我前面的判断——整个流水线是“木桶效应”VPU只是其中一块板要整体优化才扛得住更多路数。4. GPU渲染与显示同步Mali-G610的多路纹理方案4.1 纹理创建与YUV转换取舍GPU渲染阶段的核心工作是把RGA转好的RGBA8888帧上传为纹理然后按网格布局绘制。这里必须先说一个关键捷径如果走OpenGL ES的glTexImage2D直接上传CPU指针数据首先要从dma-buf映射到CPU地址空间这等于绕回了拷贝的老路性能差很多。正确做法是用EGL扩展导入dma-bufEGLint attrs[] { EGL_WIDTH, width, EGL_HEIGHT, height, EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ABGR8888, EGL_DMA_BUF_PLANE0_FD_EXT, fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, EGL_DMA_BUF_PLANE0_PITCH_EXT, stride, EGL_NONE }; EGLImage image eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attrs); glGenTextures(1, tex); glBindTexture(GL_TEXTURE_2D, tex); glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);这样纹理的数据就直接指向dma-bufGPU读的就是RGA产出的数据没有额外拷贝。这套流程需要在EGL初始化时开启EGL_KHR_image_base扩展Rockchip的Mali驱动是支持的。在纹理格式选择上我之前犹豫过要不要直接让GPU做YUV到RGB转换省掉RGA的参与。后来实测发现shader里做YUV420转RGB需要采样三个平面再套转换矩阵fragment shader开销明显增加。16路同时渲染时GPU的fragment吞吐会成为瓶颈。而RGBA8888是单纹理采样GPU的负载大幅降低。所以虽然内存占用多了2.5倍但对系统整体性能更有利。这就是为什么我坚持用RGA做预处理。4.2 多路画面布局与绘制绘制部分我用OpenGL ES 3.2为16个视频通道各分配一个纹理对象然后在一个全屏四边形上根据摄像机编号计算视口位置。渲染循环里最核心的优化点是只有当某个通道有新帧到达时才更新对应纹理没有新帧的通道继续沿用上一帧纹理避免无效的纹理更新。视口可以用一个glViewport循环来切分也可以一次把所有通道的纹理绘制到一个带有动态数组的VBO中。我选了后者——把所有通道的位置、UV坐标和纹理ID集中到一个batch里一次draw call把16个视频画面全画出来。这样渲染线程每帧只需要一次提交大大降低了CPU到GPU的同步开销。另外多路监控通常需要在画面上叠加时间戳、通道名称等OSD信息。我单独准备了一个文字纹理图集在视频层之上再画一层半透明的OSD四边形用混合模式叠加。OSD由于频率较低每秒更新一次即可不需要每帧都重新上传纹理节省了大量带宽。4.3 DRM/KMS显示与帧同步GPU绘制完成后的显示环节国内嵌入式环境最常用的有两种方式一种是EGLFSDRM后端Qt常用另一种是纯DRM/KMS直通。我选择的是后者因为在多路监控场景下控制力更强页面翻转page flip的时序完全由自己掌握。DRM/KMS的基本流程是打开/dev/dri/card0找到Connector和Encoder创建一个或多个CRTC设置分辨率和刷新率然后通过drmModePageFlip提交带DRM_MODE_PAGE_FLIP_EVENT标志的翻转请求。硬件vsync到来时翻转完成通过事件fd读取信号。多路场景下有一个典型误区以为只要GPU画好帧、提交给DRM就完事。实际上RK3588的显示控制器VOP还负责把各个图层合成。要利用好硬件合成能力可以把视频画面分成多个plane放进VOP而不是全部合成到一张大纹理里。不过这样限制比较多plane数量有限、格式要求严格所以在16路场景下我全部走GPU合成VOP只做最后的信号输出简化了复杂性。注意DRM提交必须在vsync回调中串行化不能在多个线程里同时调用page flip否则会频繁报错“flip already pending”。我专门用一个displayserver线程负责所有DRM操作渲染线程只负责把GL绘制和page flip都放在这一个线程内顺序执行实测上非常稳定。5. 关键工具链细节RGA转换、视频接入与性能分析5.1 RGA硬件转换的正确打开方式RGA是Rockchip的2D图形加速模块支持格式转换、缩放、旋转、裁剪等功能。MPP解出来的NV12帧交给RGA转RGBA8888普通的调用方式是通过librga的im2dAPI。核心代码示例#include im2d.h #include RgaUtils.h rga_info_t src, dst; memset(src, 0, sizeof(src)); memset(dst, 0, sizeof(dst)); src.fd nv12_fd; src.mmuFlag 1; src.format RK_FORMAT_YCbCr_420_SP; src.act_w width; src.act_h height; dst.fd rgba_fd; dst.mmuFlag 1; dst.format RK_FORMAT_RGBA_8888; dst.act_w width; dst.act_h height; int ret im2d_process(src, dst, RGA_ROTATE_0, RGA_BLEND_NONE, 0, 0);这里有一个必须注意的地方RGA要求输入输出buffer的宽高和stride要匹配物理内存的实际分配大小很多驱动在宽高不是16像素对齐时会出错。我的做法是解码时让MPP输出16像素对齐的宽高通过解码配置强制设置这样RGA转换就不再额外做scaling直接一一对应转换。RGA虽然是硬件加速但调用也不是零成本。每调用一次im2d_process都要经过一次ioctl到内核驱动频繁调用时系统调用开销会累积。所以在处理多路视频时我选择把RGA调用放在解码线程的下半段每个通道串行执行避免和GPU渲染线程争抢RGA硬件。RGA一次只能处理一个任务多线程并发反而会排队这个瓶颈一定要心里有数。5.2 视频接入RTSP拉流与网络抖动缓冲16路摄像头接入RTSP拉流是整个流水线的起点也是网络异常时最需要兜底的一环。我基于FFmpeg的libavformat做RTSP客户端但只用来做解封装和码流解析解码部分完全不走FFmpeg直接喂给MPP。这样保证了“FFmpeg拉流MPP硬解”的最佳搭配。网络抖动直接会导致解码端码流断断续续。我的做法是每个通道设置一个抖动缓冲队列按解析时间戳PTS排序后再送入MPP。队列长度控制在16~25个包之间网络偶尔卡一下也能平滑度过如果连续超时超过2秒就触发自动重连并向上层业务上报一次断流事件。对于RTSP本身的传输方式我默认用TCP而不是UDP虽然TCP首包建立连接会慢一些但在跨交换机、跨网段的监控网络里更稳定不会因为丢包导致马赛克。UDPRTP虽然实时性略好但在弱网环境下画质损失严重多路同时异常时排查起来会很痛苦。项目里我在配置里做了两种模式的支持默认推荐TCP。5.3 性能分析工具与调优闭环整个流水线搭建起来之后如何科学地调优不能靠猜。我主要用三组工具来做性能证据收集perfCPU热点、Rockchip的mpp_testVPU吞吐、以及最直接的cat /sys/kernel/debug/mali/0/gpu_utilisationGPU占用。用perf抓线程调度和锁竞争时我发现过一个诡异现象16路解码线程的futex等待时间很高定位后发现是MPP内部有一个解码分组锁多线程并发时会在锁上排队。解决办法是把16路按每4路一个group拆分这样锁粒度从“全全局”降为“每group”性能提升非常明显。这类问题如果不靠perf抓靠肉眼很难找出来。GPU占用率的监控我写了个小脚本定时采样gpu_utilisation和预期帧率做对比。还有一个好用的工具是drm_info可以查看DRM显示管线的具体状态比如当前提交的plane格式、是否出现vsync miss。项目调优接近尾声时连续压测48小时GPU占用稳定在40%左右内存占用维持稳定说明调优闭环已经收敛。6. 常见问题与排查技巧实录6.1 硬解画面花屏或绿屏这是多路硬解最容易踩的坑原因大概率是码流切片不完整。RTSP的RTP分包在传输过程中存在分包错序、乱序如果把不完整的NALU喂给MPP解码器会输出花屏帧。解决思路是在喂码流前做一层简单的NALU聚合把RTP包按照时间戳聚合解析出完整的H.264/H.265访问单元AU后再交给MPP。另外绿屏也可能是帧缓冲没有正确归还导致的。MPP如果长时间等不到空闲输出缓冲会直接丢弃当前帧返回空。这种情况就检查一下enqueue是否成对出现以及解码输出缓冲是否分配足够。我调试时遇到过解码16路中偶尔某一路频繁绿屏最后发现就是那一帧缓冲被占了没还回去。6.2 多路预览画面撕裂画面撕裂的直接原因是GPU渲染提交和DRM页面翻转没有同步。最彻底的解决方案是打开VOP的垂直同步vsync再做页面翻转并在GL绘制时使用EGL_SWAP_BEHAVIOR_PRESERVED保持缓冲一致性。在OpenGL ES下开启vsync需要保证eglSwapInterval(display, 1)同时DRM page flip要等drmHandleEvent收到完成事件后再提交下一帧。个别场景下撕裂还可能是RGA在转换过程中读写竞争引起的。比如RGA还没完全写完某帧数据GPU就开始读取这块dma-buf纹理。解决办法是正确使用dma-buf fenceRGA转换完成后调用im2d_fence等待完成再释放纹理引用。我在RK3588上实测加了fence之后撕裂现象基本绝迹。6.3 推流延迟持续增大监控系统里最怕的就是画面延迟越跑越大最终导致“实时画面”名存实亡。延迟变大的源头通常是解码队列持续积压VPU解帧速度跟不上输入码流速度或者RGA转换排队时间变长。排查时可以打印每路通道的队列深度日志看看有没有持续涨到上限的情况。我遇到过一次奇特问题解码和RGA都正常但延迟还是不断增加。最后定位到是GPU纹理更新逻辑写错了——每个通道每次有新帧都会重新上传一次纹理但没检查这个通道是否在可见区域导致不可见区域的纹理也被反复刷新白白耗掉带宽。修正后延迟立刻恢复正常。这也提醒我渲染循环的每一帧操作都要有“变化检测”不是每路通道都需要每帧刷新。6.4 常见问题速查表现象可能原因排查方向某一路绿屏/花屏RTP分包不完整检查NALU聚合逻辑多路预览卡顿RGA排队锁瓶颈检查是否多线程并发调用RGA页面撕裂未等待vsync确认eglSwapInterval和page flip事件串行内存持续增长dma-buf未正确释放检查每帧是否调用了EGLImage销毁延迟逐渐增大解码队列积压观察队列深度日志GPU占用突高纹理或场景缓冲更新过度检查渲染线程是否有无效重绘6.5 其他值得记录的坑RTSP拉流偶发断流重连时之前喂给MPP的码流上下文和参考帧信息会全部失效。如果不做任何处理就重连MPP会持续输出错误帧。解决办法是重连成功后需要调用MPP的解码复位接口MPP_CMD_SET_CODEC_TYPE重新设置或重新创建解码会话软复位之后重新正常解码。这步不能省。还有调试es8388音频、打包烧录这类系统层面的问题在这个项目里属于“外围杂事”但也容易耗时间。比如RK3588默认的ALSA音频通道可能被其他模块占用导致音频回调超时影响整个线程调度。如果监控系统里需要音频建议单独给音频采集分配一个实时优先级线程避免与解码线程互相干扰。7. 性能数据与项目复盘这套方案做到了什么程度整个系统跑通之后我最关心的就是具体性能数据能不能支撑未来扩展。这里分享一组正式压测时的数据稳定运行16路1080p/30fps的H.265摄像头端到端预览延迟约120~150ms。长期运行内存占用维持在900MB左右含系统、UI、纹理缓冲无持续增长。CPU整体占用率在15%~20%之间大部分消耗在RTSP封包解析和业务逻辑上。GPU主频约70%负载RGA占用约60%VPU占用约65%三者并行没有出现带宽瓶颈。整机功耗控制在12W左右不含屏幕对比传统x86方案节省约70%功耗。从这个数据看RK3588这种“VPU解码RGA转换GPU渲染”的三级流水线设计非常契合多路视频监控场景。后续如果要扩展到32路我认为瓶颈会首先出现在内存带宽上而不是运算单元。那时可能需要用更节省内存的YUV渲染方案或者开启显示层的硬件压缩功能如AFBC来降低带宽占用。还有一个有意思的观察将GPU渲染与NPU推理叠加时两者都吃Mali的算力但GPU的渲染任务和NPU的推理任务由驱动分别调度没有出现明显的互相拖累。这说明RK3588的异构调度做得不错给后续在同一设备上做AI实时分析留了充分的升级空间。8. 最后的经验之谈多路视频监控系统的开发真正花的功夫不在“跑通Demo”而在“把每一路都稳定跑好”。硬解和GPU渲染分开看都不难但把它们串在一起处理好多路并发、缓冲同步、显示时序才是这个项目的核心价值所在。我个人的体会是入手这类项目一定要先画清楚数据流图把每个硬件单元负责什么、数据怎么流转画明白。不要一上来就调代码否则多路问题叠加在一起排查起来会十分痛苦。另外内存和dma-buf的生命周期管理要提前规划好这是最容易出隐患也最难排查的地方。如果你也在用RK3588做类似的视频产品建议按照“先单路跑通、再多路压测、最后做异常注入”的顺序推进。单路跑通能确认SDK链路没有问题多路压测能暴露出带宽和锁竞争问题异常注入断网、弱网、长时间运行则是检验系统稳定性的关键一步。最后再分享一个小技巧多路监控场景下日志系统一定要做成“分级滚动”不要把所有调试信息都打印到终端否则I/O会让解码链路偶尔停顿。我用的是spdlog库每个通道一个logger按天滚动日志级别在生产环境默认设为info。这样既保留了排查问题的线索又不至于影响性能。希望这篇实战记录能帮你少走一些弯路。
返回列表