ARTICLE DETAIL

资讯详情

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

RK3588多路视频拼接实战:RGA与GPU协同实现低延迟4K输出

RK3588多路视频拼接实战:RGA与GPU协同实现低延迟4K输出 1. 项目缘起与整体设计思路1.1 为什么要在RK3588上做多路视频拼接RK3588这颗芯片在边缘视觉领域的热度一直没降过。8核CPU4×A764×A55、Mali-G610 MP4 GPU、独立NPU6 TOPS、还有专门做2D图像处理的RGA模块加上VPU硬解码基本上把一路视频从“进来”到“出去”的每个环节都配了专用硬件。我手头这个项目需求很直接4路1080P摄像头输入实时拼接成一路4K画面输出到HDMI大屏要求端到端延迟控制在80ms以内CPU占用率不超过30%。一开始想得很简单——用OpenCV的hconcat拼一下不就行了实测下来4路1080P用CPU做拼接光memcpy和颜色空间转换就能把A76核心吃到70%以上帧率还稳不住30fps。问题出在数据通路上摄像头出来的NV12数据要先转成BGR给OpenCV处理拼完再转回NV12给显示子系统这两次转换加上拼接本身的内存拷贝全是CPU在扛。后来把思路转到RGA和GPU协同上整个链路才跑通。核心逻辑是RGA负责所有2D变换类操作裁剪、缩放、格式转换、旋转、叠加GPU负责需要3D管线或复杂采样的合成操作CPU只做调度和同步。这样下来CPU占用降到12%左右4路拼接稳定在60fps延迟压到55ms。1.2 RGA与GPU的分工边界在哪里很多人会问RGA也能做合成GPU也能做合成到底怎么分我的经验是看三个维度——数据格式、变换复杂度、输出精度要求。RGARaster Graphic Acceleration本质是一个2D位图加速器擅长的是格式转换NV12↔RGB888↔RGBA8888缩放支持多级缩放质量可调裁剪与ROI提取简单叠加alpha blend、colorkey旋转90/180/270度GPUMali-G610擅长的是多纹理采样与混合透视变换、非线性几何校正需要shader自定义的像素级处理高精度插值双三次以上在这个项目里4路摄像头的原始数据是NV12需要先裁剪掉边缘无效区域再缩放到目标尺寸然后拼到4K画布的指定位置。裁剪缩放格式转换这三步全部交给RGA拼接到画布这一步用GPU做——因为GPU可以用一个draw call把4个纹理一次性渲染到framebuffer比RGA做4次blit再合成要高效。注意RGA的blit操作是同步的每次调用都有驱动开销。如果4路分别做4次RGA blit到同一块buffer驱动层的串行化会让耗时线性增长。用GPU做最终合成可以把这4次操作合并成一次渲染。1.3 数据通路的整体架构整个链路我画过很多版最终稳定下来的结构是这样的Camera → V4L2 → MPP解码 → RGA(裁剪缩放格式转换) → DMA-BUF → GPU合成 → DRM显示关键点在于DMA-BUF的零拷贝传递。RGA处理完的输出buffer不回到用户态直接以DMA-BUF fd的形式传给GPU的EGL imageGPU渲染完再以DMA-BUF形式送给DRM framebuffer。整条链路没有一次CPU memcpy这是延迟能压到55ms的根本原因。如果中间任何一步走了memcpy或者glTexImage2D从CPU内存上传纹理延迟立刻会涨到120ms以上CPU占用也会飙升。这个坑我踩过后面会详细说。2. 核心细节解析与实操要点2.1 RGA的配置参数怎么调RGA的API在不同内核版本和用户态库版本之间有差异我用的是Rockchip官方linux-rga库librga.so内核是5.10。核心结构体是im2d_api里的rga_buffer_t和im_rect。一次典型的RGA调用长这样rga_buffer_t src wrapbuffer_fd(src_fd, src_w, src_h, RK_FORMAT_YCbCr_420_SP); rga_buffer_t dst wrapbuffer_fd(dst_fd, dst_w, dst_h, RK_FORMAT_RGB_888); im_rect src_rect {crop_x, crop_y, crop_w, crop_h}; im_rect dst_rect {0, 0, dst_w, dst_h}; IM_STATUS status improcess(src, dst, {}, src_rect, dst_rect, {}, IM_SYNC);几个关键参数的选择逻辑缩放算法RGA支持IM_INTERP_DEFAULT、IM_INTERP_BILINEAR、IM_INTERP_BICUBIC。1080P缩到960×540这种比例用bilinear就够了bicubic会多耗约30%的RGA时间但肉眼几乎看不出差别。如果是缩到很小比如1/4以下bicubic的收益才明显。格式选择RGA输出RGB888比输出RGBA8888快约15%因为少了一个通道的写入带宽。但GPU做纹理采样时RGBA8888对齐更好实测下来如果RGA输出RGB888再让GPU采样驱动内部可能会做一次隐式转换反而更慢。所以最终我统一用RGBA8888。同步模式IM_SYNC是阻塞等待完成IM_ASYNC是异步提交。4路并行处理时用IM_ASYNC提交4个任务然后用imsync()统一等待比串行IM_SYNC快约40%。2.2 GPU合成的EGL配置GPU这边用的是OpenGL ES 3.1 EGL。核心是把RGA输出的DMA-BUF导入为EGLImage再绑定到纹理。EGLint attrs[] { EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ARGB8888, EGL_WIDTH, width, EGL_HEIGHT, height, EGL_DMA_BUF_PLANE0_FD_EXT, fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, EGL_DMA_BUF_PLANE0_PITCH_EXT, pitch, EGL_NONE }; EGLImageKHR image eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attrs); glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);这里有几个容易翻车的点pitch对齐RGA输出的pitch不一定是width×4可能是对齐到64字节或256字节的值。如果EGL的pitch填错画面会出现斜条纹。必须用RGA返回的实际pitch或者用librga的querystring接口查。格式匹配RGA输出RK_FORMAT_RGBA_8888对应DRM格式是DRM_FORMAT_ABGR8888注意字节序对应EGL是EGL_LINUX_DRM_FOURCC_EXT填DRM_FORMAT_ABGR8888。这个对应关系搞错的话颜色通道会乱红色变蓝色。纹理参数GL_TEXTURE_MIN_FILTER和GL_TEXTURE_MAG_FILTER都设GL_LINEARGL_TEXTURE_WRAP_S/T设GL_CLAMP_TO_EDGE。如果设了GL_REPEAT边缘会出现采样溢出。2.3 多路同步与帧率控制4路摄像头如果各自跑各自的帧率拼接出来的画面会有撕裂感。我的做法是用一个统一的vsync信号驱动整个管线DRM的page flip事件作为主时钟每次page flip后检查4路RGA任务是否都完成全部完成后触发GPU渲染GPU渲染完提交DRM等待下一次page flip这样天然就是60fps的节奏而且不会出现某一路数据没准备好就渲染的情况。如果某一路RGA超时比如摄像头丢帧就复用上一帧的纹理保证画面不闪。实操心得RGA任务超时阈值设成8ms比较合适。60fps下每帧预算16.6msRGA正常耗时3-5ms留8ms给异常情况。超过8ms没完成就判定丢帧直接复用旧纹理。3. 实操过程与核心环节实现3.1 环境准备与依赖安装板子跑的是Rockchip官方Ubuntu 22.04内核5.10需要确认几个关键库的版本# 检查RGA库 ls /usr/lib/aarch64-linux-gnu/librga.so* # 检查MPP ls /usr/lib/aarch64-linux-gnu/librockchip_mpp.so* # 检查DRM ls /usr/lib/aarch64-linux-gnu/libdrm.so* # 检查EGL ls /usr/lib/aarch64-linux-gnu/libEGL.so*如果用的是Buildroot或者自己裁的系统可能需要从Rockchip的github仓库编译这些库。编译librga时注意--enable-shared和--enable-loglog在调试阶段很有用。内核方面需要确认几个configCONFIG_ROCKCHIP_RGAy CONFIG_DRM_ROCKCHIPy CONFIG_MALI_BIFROSTy CONFIG_VIDEO_ROCKCHIP_MPPy/dev/rga、/dev/dri/card0、/dev/mpp_service这几个设备节点必须存在。3.2 摄像头采集与MPP解码4路摄像头通过MIPI-CSI接入V4L2采集出来是NV12格式。如果摄像头输出的是H.264/H.265码流需要走MPP解码。我这边用的是直接输出NV12的sensor省掉了解码环节。V4L2的配置关键点struct v4l2_format fmt {0}; fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; fmt.fmt.pix_mp.width 1920; fmt.fmt.pix_mp.height 1080; fmt.fmt.pix_mp.pixelformat V4L2_PIX_FMT_NV12; fmt.fmt.pix_mp.num_planes 1;buffer用VIDIOC_REQBUFS申请memory设V4L2_MEMORY_MMAP或V4L2_MEMORY_DMABUF。用DMABUF的话可以直接把fd传给RGA省一次映射。注意4路同时开的时候MIPI-CSI的带宽要算清楚。1080P60fps NV12的带宽是1920×1080×1.5×60≈186MB/s4路就是745MB/s。RK3588的ISP和VICAP能扛住但DDR带宽要留够余量否则会出现丢帧。3.3 RGA处理流水线每路摄像头的处理流程// 1. 从V4L2 dequeue拿到buffer fd int src_fd get_v4l2_buffer_fd(); // 2. 配置RGA源和目标 rga_buffer_t src wrapbuffer_fd(src_fd, 1920, 1080, RK_FORMAT_YCbCr_420_SP); rga_buffer_t dst wrapbuffer_fd(dst_fd, 960, 540, RK_FORMAT_RGBA_8888); // 3. 裁剪掉边缘16像素缩放到960×540 im_rect src_rect {16, 16, 1920-32, 1080-32}; im_rect dst_rect {0, 0, 960, 540}; // 4. 异步提交 improcess(src, dst, {}, src_rect, dst_rect, {}, IM_ASYNC);4路都提交完后调imsync()等待全部完成。实测4路并行RGA耗时约4.2ms串行的话要12ms以上。目标buffer的分配用dma_heapint dst_fd dma_heap_alloc(960 * 540 * 4);用dma_heap而不是malloc是因为要保证物理连续RGA和GPU才能直接访问。3.4 GPU合成与DRM显示GPU这边用一个简单的shader把4个纹理画到4K framebuffer的四个象限// vertex shader attribute vec2 a_pos; attribute vec2 a_texcoord; varying vec2 v_texcoord; void main() { gl_Position vec4(a_pos, 0.0, 1.0); v_texcoord a_texcoord; } // fragment shader precision mediump float; uniform sampler2D u_texture; varying vec2 v_texcoord; void main() { gl_FragColor texture2D(u_texture, v_texcoord); }4个象限分别用不同的MVP矩阵和纹理画4次。或者用一个纹理数组GL_TEXTURE_2D_ARRAY一次画完但Mali-G610对纹理数组的支持一般实测4次draw call反而更快。渲染完的framebuffer通过DRM的drmModeAddFB2创建framebuffer然后drmModePageFlip提交显示。drmModeAddFB2(fd, 3840, 2160, DRM_FORMAT_ABGR8888, handles, pitches, offsets, fb_id, 0); drmModePageFlip(fd, crtc_id, fb_id, DRM_MODE_PAGE_FLIP_EVENT, NULL);3.5 性能实测数据最终实测数据4路1080P输入4K输出60fps环节耗时CPU占用V4L2采集1.2ms2%RGA处理4路并行4.2ms0%硬件GPU合成3.8ms0%硬件DRM提交0.5ms1%调度与同步2.1ms9%总计11.8ms12%端到端延迟55ms其中采集缓冲占2帧33ms处理链路占11.8ms显示缓冲占1帧16.6ms。4. 常见问题与排查技巧实录4.1 画面撕裂与同步问题现象拼接画面出现水平撕裂或者某一路画面比其他路慢半拍。排查思路先确认4路RGA是否都用了IM_ASYNC并且统一imsync()。如果某一路用了IM_SYNC它会阻塞其他路的提交。检查DRM的page flip事件是否正常收到。如果没收到就提交下一帧会出现tearing。确认GPU渲染和DRM提交在同一个线程跨线程提交会有竞态。解决用drmHandleEvent处理page flip事件收到后再触发下一帧的渲染。整个管线是“事件驱动”而不是“定时驱动”。4.2 RGA输出颜色异常现象画面偏色红色变蓝或者整体发绿。排查思路检查RGA的源格式和目标格式是否匹配。NV12的Y和UV plane顺序搞反会导致颜色完全错乱。检查EGL的DRM_FORMAT和RGA的RK_FORMAT对应关系。RK_FORMAT_RGBA_8888对应DRM_FORMAT_ABGR8888不是DRM_FORMAT_RGBA8888。检查pitch是否填对。pitch填错会出现斜条纹而不是偏色但两者容易混淆。解决写一个测试程序RGA输出纯红色RGBA255,0,0,255然后用drmModeDumbBuffer读回来检查字节序。4.3 GPU合成性能不达标现象GPU合成耗时超过10ms帧率掉到30fps以下。排查思路检查是否用了glTexImage2D从CPU上传纹理。这个操作会触发一次完整的memcpy4路1080P就是8MB的数据拷贝耗时至少5ms。检查是否每次渲染都重新创建EGLImage。EGLImage的创建和销毁开销很大应该缓存复用。检查shader是否过于复杂。如果用了多重采样或者复杂的混合模式Mali-G610的填充率会不够。解决用DMA-BUF导入纹理缓存EGLImageshader保持最简。实测优化后GPU合成从12ms降到3.8ms。4.4 常见问题速查表问题现象可能原因排查方法解决方案画面撕裂page flip未同步检查drmHandleEvent事件驱动渲染颜色偏色格式对应错误纯色测试核对RK_FORMAT与DRM_FORMAT斜条纹pitch填错打印实际pitch用RGA返回的pitch帧率不稳RGA串行执行检查IM_SYNC/IM_ASYNC统一用IM_ASYNCGPU耗时高CPU上传纹理检查glTexImage2D改用DMA-BUF导入某路黑屏V4L2 buffer未dequeue检查VIDIOC_DQBUF确保每帧都dq延迟高中间有memcpy用perf跟踪全链路DMA-BUFRGA超时带宽不足检查DDR频率提高DDR频率或降分辨率4.5 独家避坑技巧技巧一RGA的imcheck接口。在正式调用improcess之前先用imcheck检查参数是否合法。这个接口会返回具体的错误码比improcess失败后猜原因要高效得多。技巧二DMA-BUF的fd缓存。每次dma_heap_alloc都有开销建议预分配一组buffer循环使用。我这边是每路预分配3个buffer形成生产者-消费者队列。技巧三GPU的glFinish慎用。glFinish会强制等待GPU完成破坏管线的并行性。用glFenceSync加eglClientWaitSync可以精确等待某一帧完成而不阻塞整个管线。技巧四DRM的atomic提交。如果显示子系统支持atomic模式用drmModeAtomicCommit比drmModePageFlip更灵活可以一次性提交多个plane的更新。RK3588的VOP2支持atomic。技巧五温度监控。4路RGAGPU满负荷跑RK3588的SoC温度会到70度以上。如果散热不好会触发降频帧率会突然掉。建议加一个温度监控线程超过75度就降帧率到30fps。5. 扩展方向与个人体会这套方案跑通之后我陆续做了几个扩展。一个是把拼接布局做成可配置的支持2×2、13、画中画等模式通过修改GPU的MVP矩阵就能实现不用改RGA部分。另一个是加了OSD叠加用RGA的alpha blend把时间戳和路名叠到每路画面上RGA做这个几乎不耗额外时间。还有一个方向是接入NPU做智能分析。RK3588的NPU和RGA、GPU是独立硬件可以并行工作。我试过在拼接的同时跑YOLOv8做目标检测NPU占用约40%对拼接帧率没有影响。数据通路是RGA输出一份给GPU合成同时输出一份给NPU推理两份数据用同一个DMA-BUF零拷贝。踩过最大的坑是DMA-BUF的cache一致性问题。RGA写完的数据GPU读的时候如果cache没同步会读到旧数据。解决方案是用DMA_BUF_IOCTL_SYNC做cache flush或者申请buffer时用DMA_HEAP_UNCACHED标志。这个问题在x86上不常见但在ARM上很典型。最后分享一个调试技巧用perf和ftrace跟踪RGA和GPU的驱动调用能看到每次操作的精确耗时。/sys/kernel/debug/rga下面有RGA的调试节点可以看到任务队列和硬件占用率。GPU这边用/sys/class/devfreq/fde60000.gpu/可以看到频率和负载。这些信息对定位性能瓶颈非常有用。
返回列表