1. 问题背景
在 RK3588 平台上构建多路 RTSP 视频流处理管线时,流水线如下:
RTSP → FFmpeg 拉流 → MPP 硬解码 → RGA 缩放与色彩转换 → 640×640 RGB 输出
启用 RGA 后,系统频繁出现驱动报错,严重时直接触发内核异常重启。典型用户态日志如下(整理自实际输出):
RgaBlit(1528) RGA BLIT fail: Invalid argument handle: fd:0, virt:0x7f254637b0, phys:0, format:0 rect[0,0,1920,1080,1920,1080,2560,0] rect[0,0,1920,1080,1920,1080,0,0] ... RgaBlit(1527) RGA_BLIT fail: Invalid argument
同时,内核 dmesg 中刷出大量 RGA MMU 错误,并伴随内存管理 BUG:
RGA_MMU unsupported memory Larger than 4G! rga_mm_map_buffer map virtual address error! rga mm buffer map failed! src channel map job buffer failed! rga_job_commit: failed to map job info rga request submit failed! BUG: Bad rss-counter state mm:00000000b3118dae type:MM_FILEPAGES val:30096 BUG: non-zero pgtables bytes on freeing mm: 1638400
该问题与 RTSP 服务端、FFmpeg 拉流参数、MPP 解码器实例数无关,焦点指向 RGA 的内存使用方式。
2. 根因分析
错误的核心在于RGA 目标图像使用了 C++ 堆内存,即std::vector<uint8_t>分配的缓冲区。问题复现代码如下:
std::vector<uint8_t> rga_output_(kOutputWidth * kOutputHeight * 3); rga_buffer_t dst = wrapbuffer_virtualaddr( rga_output_.data(), kOutputWidth, kOutputHeight, RK_FORMAT_RGB_888);在 64 位 Linux 系统中,堆上分配的大块内存的虚拟地址通常高于 4 GB(例如0x7f254637b0)。而 RK3588 当前板端 RGA 驱动的虚拟地址映射能力受限,无法处理地址空间高于 4 GB 的用户态 buffer。因此,当调用improcess()提交 RGA 任务时,内核在rga_mm_map_buffer阶段映射失败,触发:
RGA_MMU unsupported memory Larger than 4G! map virtual address error!任务提交失败后,驱动内部资源未正确回收,进一步引发rss-counter异常乃至内核 Oops 重启。这一问题无法通过忽略improcess()返回值规避,必须从根本上改变 RGA 使用的内存类型。
3. 修复原则
核心思路:不再将普通 CPU 虚拟地址直接交给 RGA。利用 Rockchip MPP 提供的 DRM DMA‑BUF 作为 RGA 的输入和输出。
错误方式:wrapbuffer_virtualaddr(std::vector::data(), ...) 正确方式:MPP 分配 MPP_BUFFER_TYPE_DRM → 获得 DMA‑BUF fd → wrapbuffer_fd(fd, ...)
新的数据通路:
MPP decoder output MppFrame → MppBuffer → source DMA‑BUF fd ↓ RGA input ↓ MPP DRM allocation → destination DMA‑BUF fd → RGA output
此时 RGA 的输入和输出均由 DMA‑BUF fd 描述,绕过了高地址虚拟内存的硬件限制。
4. 具体代码修复
以下改动基于项目实际代码,涉及头文件和核心处理逻辑。
4.1 增加成员变量
在StreamDecoder类中添加:
MppBufferGroup frame_group_ = nullptr; // MPP 解码输出的 DRM buffer group MppBufferGroup rga_output_group_ = nullptr; // RGA 输出专用的 DRM buffer group MppBuffer rga_output_buffer_ = nullptr; // 复用的一块 RGA 输出 DMA‑BUF4.2 获取 MPP 解码帧的 DMA‑BUF fd
在每一帧处理时,从解码得到的MppFrame中取出MppBuffer,再通过mpp_buffer_get_fd()获取文件描述符:
MppBuffer buffer = mpp_frame_get_buffer(frame); if (!buffer) return false; int source_fd = mpp_buffer_get_fd(buffer); if (source_fd < 0) return false; // 同时获取真实宽高与 stride,用于后续 RGA 设置 int width = mpp_frame_get_width(frame); int height = mpp_frame_get_height(frame); int h_stride = mpp_frame_get_hor_stride(frame); int v_stride = mpp_frame_get_ver_stride(frame);4.3 为 RGA 输出分配 DRM DMA‑BUF
每个StreamDecoder实例独占一块 640×640 RGB 输出 buffer,仅分配一次,后续帧复用:
const size_t output_size = kOutputWidth * kOutputHeight * 3; // RGB888 if (!rga_output_buffer_) { // 1. 创建 DRM 类型的 buffer group if (mpp_buffer_group_get_internal(&rga_output_group_, MPP_BUFFER_TYPE_DRM) != MPP_OK) return false; // 2. 从 group 中申请 buffer if (mpp_buffer_get(rga_output_group_, &rga_output_buffer_, output_size) != MPP_OK) { mpp_buffer_group_put(rga_output_group_); rga_output_group_ = nullptr; return false; } } int destination_fd = mpp_buffer_get_fd(rga_output_buffer_); if (destination_fd < 0) return false;4.4 使用 fd 构造 RGA buffer 并执行操作
rga_buffer_t src = wrapbuffer_fd(source_fd, h_stride, v_stride, RK_FORMAT_YCbCr_420_SP); rga_buffer_t dst = wrapbuffer_fd(destination_fd, kOutputWidth, kOutputHeight, RK_FORMAT_RGB_888); im_rect src_rect = {0, 0, width, height}; im_rect dst_rect = {0, 0, kOutputWidth, kOutputHeight}; IM_STATUS ret = improcess(src, dst, {}, src_rect, dst_rect, {}, IM_SYNC); return (ret == IM_STATUS_SUCCESS);关键点:
src传入h_stride / v_stride,避免使用图像真实宽高导致寻址错误;
src_rect使用真实宽高width / height限制有效像素区域;
dst固定为 640×640,dst_rect与输出尺寸一致;
IM_SYNC确保函数返回时 RGA 已写入完毕,后续可立即消费。
4.5 资源释放
在close_pipeline()或析构时,必须先释放 buffer 再释放 group:
if (rga_output_buffer_) { mpp_buffer_put(rga_output_buffer_); rga_output_buffer_ = nullptr; } if (rga_output_group_) { mpp_buffer_group_put(rga_output_group_); rga_output_group_ = nullptr; }RTSP 重连、worker 退出等场景均会调用该释放逻辑,避免残留 DMA‑BUF。
5. 格式与 stride 注意事项
MPP 解码输出通常为 NV12 (
RK_FORMAT_YCbCr_420_SP),必须与 RGA 输入格式严格一致;wrapbuffer_fd()的宽高应使用buffer stride(h_stride,v_stride),而非图像真实宽高;im_rect中的宽高则必须为真实图像宽高,否则会出现花屏或越界;目标尺寸和格式修改时(如变为 RGBA8888),需同步更新
output_size和wrapbuffer_fd的格式参数。
6. 并发与多路流下的 buffer 管理
当前设计为每路流持有单个 RGA 输出 DMA‑BUF,并使用同步模式IM_SYNC。只要在improcess返回后立即同步消费(如推理、编码),就不会发生数据竞争。
如果后续引入异步消费者(例如独立推理线程),单 buffer 会因覆盖而损坏数据。此时需升级为多 buffer 环形队列,并添加状态标志(空闲 / 写入中 / 使用中)。
9. 总结
本文记录了一次典型的 RK3588 RGA 高地址虚拟内存崩溃问题,其根源在于将 4 GB 以上的堆内存直接传递给 RGA 驱动,而驱动无法映射该地址。解决方案是全面切换为 MPP DRM DMA‑BUF,使 RGA 通过文件描述符操作物理连续且地址可控的缓冲区。修复后的管线稳定可靠,同时也为后续直接对接 RKNN 等硬件加速器铺平了道路。
如果你的场景中仍然不得不使用堆内存,可尝试通过
mpp_buffer_import或设置系统vm.overcommit_memory等方式将堆内存“伪装”成 DMA‑BUF,但此类方法并非官方推荐,极易引入新的兼容性问题。最稳妥的方案仍是文中所述的全 DRM DMA‑BUF 链路。