瑞芯微开发笔记 · MPP 篇(二)MPP 核心概念

图片[1]-瑞芯微开发笔记 · MPP 篇(二)MPP 核心概念-天煜博客

0. 心智模型

把 MPP 想成一个”硬件编解码器的 SDK”。它的对外 API 围绕几个不透明类型转:

MppCtx    ─── 一个解码器或编码器实例的"句柄",里面藏着所有内部状态
MppApi   ─── 一张函数指针表,所有方法挂这上面(mpi->decode_put_packet 等)
MppPacket ─── "压缩域数据" 的载体(编码后的码流字节)
MppFrame ─── "未压缩域数据" 的载体(YUV/RGB 帧)
MppBuffer ─── 一块物理连续/DMA 友好的内存,用来真正承载 packet/frame 的字节
MppMeta   ─── packet/frame 上挂的"键值对补充信息"
MppTask   ─── advanced 模式下流入流出的"工作单元"

形象点:MppCtx 是肉,MppApi 是手,MppPacket/MppFrame 是筷子和碗,MppBuffer 是碗里盛的饭。

1. MppCtx + MppApi —— 上下文 + 方法表

MppCtx ctx = NULL;
MppApi *mpi = NULL;

mpp_create(&ctx, &mpi);        // 同时拿到上下文和方法表
mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC);   // 指定:解码器,H.264

// 之后所有方法都通过 mpi 调用
mpi->decode_put_packet(ctx, packet);
mpi->decode_get_frame(ctx, &frame);
mpi->control(ctx, MPP_DEC_SET_CFG, cfg);
mpi->reset(ctx);
mpi->poll(ctx, MPP_PORT_INPUT, MPP_POLL_BLOCK);
mpi->dequeue(ctx, MPP_PORT_INPUT, &task);
mpi->enqueue(ctx, MPP_PORT_INPUT, task);

// 收尾
mpi->reset(ctx);
mpp_destroy(ctx);

为什么要拆成 ctx + mpi 两个? —— 历史原因 + ABI 友好。mpi 是版本化的方法表,库内部更新版本时可以保留旧 mpi 给老调用者。

MppCtxType 取值:

  • MPP_CTX_DEC —— 解码器
  • MPP_CTX_ENC —— 编码器

MppCodingType 常用:

  • MPP_VIDEO_CodingAVC (H.264)
  • MPP_VIDEO_CodingHEVC (H.265)
  • MPP_VIDEO_CodingMJPEG (JPEG,每帧独立)
  • MPP_VIDEO_CodingVP8/9MPP_VIDEO_CodingAV1

2. MppPacket —— 压缩域字节包

承载编码后的码流。

MppPacket packet = NULL;

// 方式 1:空包,运行时再绑数据(解码 simple 模式常用)
mpp_packet_init(&packet, NULL, 0);
mpp_packet_set_data(packet, my_data_ptr);
mpp_packet_set_size(packet, my_data_size);
mpp_packet_set_pos(packet, my_data_ptr);
mpp_packet_set_length(packet, my_data_size);

// 方式 2:用 MppBuffer 初始化(advanced 模式 / 需要 DMA 时)
mpp_packet_init_with_buffer(&packet, mppbuf);

// 标记结束
mpp_packet_set_eos(packet);

// 释放
mpp_packet_deinit(&packet);

pos vs datalength vs size 的区别

  • data / size 描述整块缓冲区(起点 + 容量)
  • pos / length 描述当前有效数据(起点 + 长度)

为什么要分?因为 MPP 内部消费数据时会移动 pos 表示”我消费到这里了”。一帧没解完时,pos 已前进,length 减少;剩下的数据下次再喂。这个设计让 mpp_packet_set_eos 的 EOS 标志可以独立于消费进度存在。

解码端 vs 编码端

解码端 (DEC)编码端 (ENC)
MppPacket 角色输入:压缩码流送进解码器输出:编码器吐出的码流
MppFrame 角色输出:解码出来的像素帧输入:原始像素帧送进编码器

在解码 demo 里见到的 decode_put_packet(ctx, packet) —— 把码流送进去; 在编码 demo 里见到的 encode_get_packet(ctx, &packet) —— 把码流取出来。 方向相反。

3. MppFrame —— 像素域帧

承载解码后或待编码的原始像素。比 packet 字段多得多:

MppFrame frame = NULL;
mpp_frame_init(&frame);

// 解码端:解码器自己 set,调用者 get 来读
RK_U32 w = mpp_frame_get_width(frame);
RK_U32 h = mpp_frame_get_height(frame);
RK_U32 hor_stride = mpp_frame_get_hor_stride(frame);  // 行字节数(含对齐填充)
RK_U32 ver_stride = mpp_frame_get_ver_stride(frame);  // 行数(含对齐填充)
MppFrameFormat fmt = mpp_frame_get_fmt(frame);
MppBuffer buf = mpp_frame_get_buffer(frame);
RK_U32 buf_size = mpp_frame_get_buf_size(frame);
RK_U32 err_info = mpp_frame_get_errinfo(frame);
RK_U32 discard = mpp_frame_get_discard(frame);
RK_U32 eos = mpp_frame_get_eos(frame);
RK_U32 info_change = mpp_frame_get_info_change(frame);

// 编码端:调用者 set,编码器读
mpp_frame_set_width(frame, p->width);
mpp_frame_set_height(frame, p->height);
mpp_frame_set_hor_stride(frame, p->hor_stride);
mpp_frame_set_ver_stride(frame, p->ver_stride);
mpp_frame_set_fmt(frame, p->fmt);
mpp_frame_set_buffer(frame, my_buffer);
mpp_frame_set_eos(frame, p->frm_eos);

// 释放
mpp_frame_deinit(&frame);

关键概念:width vs hor_stride

字段含义
width / height视频内容的显示宽高(从 SPS 读到的)
hor_stride / ver_stride缓冲区的实际宽高(含硬件对齐 padding)

例:1080p H.264 流,width=1920, height=1080,但芯片要求 16 对齐,所以 hor_stride=1920, ver_stride=1088。读 YUV 文件时必须按 stride 走,不是 width。utils 里的 read_image() / dump_mpp_frame_to_file() 已经处理了这事,但你自己写代码必须当回事。

info_change 帧

解码 simple 模式里出现的”特殊帧”:第一帧不是真实数据,而是解码器告诉你”我从 SPS 读到尺寸/格式了,请准备 buffer”。代码模式:

ret = mpi->decode_get_frame(ctx, &frame);
if (frame) {
   if (mpp_frame_get_info_change(frame)) {
       // 是 info_change 帧,根据它的 width/height/buf_size 准备 buffer pool
       RK_U32 buf_size = mpp_frame_get_buf_size(frame);
       MppBufferGroup grp = dec_buf_mgr_setup(...);
       mpi->control(ctx, MPP_DEC_SET_EXT_BUF_GROUP, grp);
       mpi->control(ctx, MPP_DEC_SET_INFO_CHANGE_READY, NULL);
  } else {
       // 真帧,处理 frame 内容
  }
   mpp_frame_deinit(&frame);
}

MPP_DEC_SET_INFO_CHANGE_READY 这个 control 是关键:调用它前解码器是”暂停”状态等你给 buffer,调用后才继续解码。

4. MppBuffer + MppBufferGroup —— DMA 友好内存

视频编解码涉及大块内存(一帧 1080p YUV420 ≈ 3 MB),还要在硬件和 CPU 间共享。普通 malloc 不行,必须是物理连续或 IOMMU 映射的 DMA buffer。MPP 用 MppBuffer 抽象。

MppBufferGroup grp = NULL;
mpp_buffer_group_get_internal(&grp, MPP_BUFFER_TYPE_DRM | MPP_BUFFER_FLAGS_CACHABLE);

MppBuffer buf = NULL;
mpp_buffer_get(grp, &buf, size);     // 从 group 里申请一块
void *ptr = mpp_buffer_get_ptr(buf); // 拿 CPU 可见的地址
size_t sz = mpp_buffer_get_size(buf);

// CPU 写入前后做 cache 同步
mpp_buffer_sync_begin(buf);
memcpy(ptr, src, sz);
mpp_buffer_sync_end(buf);

// 释放(其实是 ref-- 还回 group)
mpp_buffer_put(buf);

// group 收尾
mpp_buffer_group_put(grp);

MPP_BUFFER_TYPE_* 选项:

  • MPP_BUFFER_TYPE_DRM —— DRM PRIME,新内核 + 跨进程零拷贝最优
  • MPP_BUFFER_TYPE_ION —— ION(旧 Android 通用)
  • MPP_BUFFER_TYPE_DMA_HEAP —— 新 dma-heap 接口
  • MPP_BUFFER_TYPE_NORMAL —— 退化到 malloc,性能最差,仅 fallback

MPP_BUFFER_FLAGS_CACHABLE 让 buffer 走 CPU cache,CPU 读写快但用前要 sync。不带这个 flag 是 uncached,CPU 慢但不用 sync。

解码 buffer 的三种模式

mpi_dec_utils.h 的注释(86–91 行)讲得很清楚,这里复述:

模式含义优点缺点
MPP_DEC_BUF_INTERNAL解码器内部全自动管 buffer最简单内存用量不可控,关上下文前 buffer 没回收会泄漏/崩
MPP_DEC_BUF_HALF_INT(默认)调用者根据 info_change 申请 group 给解码器平衡仍受限于 group 上限
MPP_DEC_BUF_EXTERNAL调用者从外部分配器导入(如 Android SurfaceFlinger 的 dmabuf)零拷贝最优难写,需要外部知道 buffer size

测试代码默认走 half-internal,对应 dec_buf_mgr_setup(buf_mgr, buf_size, 24, cmd->buf_mode) 这行——24 是 buffer 数量上限。

5. MppMeta —— 挂在 packet/frame 上的键值对

很多”附加信息”不属于 packet/frame 本身的字节内容,但又得跟着流走。MPP 给每个 packet/frame 配了一个内嵌的 hash 表,叫 MppMeta,键是 enum,值可以是 s32/s64/ptr/frame/packet/buffer。

MppMeta meta = mpp_frame_get_meta(frame);
RK_S32 temporal_id = 0;
mpp_meta_get_s32(meta, KEY_TEMPORAL_ID, &temporal_id);

// 编码端:把 OSD 数据挂到 frame 上,编码器内部读
mpp_meta_set_ptr(meta, KEY_OSD_DATA, &p->osd_data);
mpp_meta_set_buffer(meta, KEY_MOTION_INFO, priv->md_info);

// 编码端:把"输出 packet 用哪个 buffer"塞给输入 frame
mpp_meta_set_packet(meta, KEY_OUTPUT_PACKET, packet);

常见 KEY 一览:

  • KEY_INPUT_PACKET / KEY_OUTPUT_FRAME —— advanced 模式下 task 里关联的输入输出
  • KEY_INPUT_FRAME / KEY_OUTPUT_PACKET —— 编码端 frame ↔ packet 关联
  • KEY_TEMPORAL_ID —— SVC/分层编码的层 ID
  • KEY_LONG_REF_IDX —— LTR(长期参考帧)索引
  • KEY_ENC_AVERAGE_QP / KEY_ENC_BPS_RT / KEY_ENC_SSE —— 编码后统计
  • KEY_OSD_DATA / KEY_OSD_DATA3 —— OSD 叠加数据
  • KEY_MOTION_INFO —— 编码副产物:运动信息输出
  • KEY_USER_DATA / KEY_USER_DATAS —— SEI 用户数据

用 meta 而不是把字段加到 MppFrame 里,是为了 ABI 稳定:新增功能时给个新 KEY,老调用者不用重编。

6. MppTask —— advanced 模式的工作单元

advanced 模式(JPEG 解码、某些编码场景)不用 put/get,而是用 task 队列:

// 输入端
mpi->poll(ctx, MPP_PORT_INPUT, MPP_POLL_BLOCK);   // 等输入端有空位
mpi->dequeue(ctx, MPP_PORT_INPUT, &task);          // 取一个空 task
mpp_task_meta_set_packet(task, KEY_INPUT_PACKET, packet);
mpp_task_meta_set_frame (task, KEY_OUTPUT_FRAME,  frame);
mpi->enqueue(ctx, MPP_PORT_INPUT, task);           // 提交

// 输出端
mpi->poll(ctx, MPP_PORT_OUTPUT, MPP_POLL_BLOCK);
mpi->dequeue(ctx, MPP_PORT_OUTPUT, &task);
mpp_task_meta_get_frame(task, KEY_OUTPUT_FRAME, &frame_out);
mpi->enqueue(ctx, MPP_PORT_OUTPUT, task);          // 还回去

简单说:dequeue 获取空 slot,填好数据,enqueue 提交。 类似 Vulkan 的 command buffer 模型。

代码里只有 mpi_dec_multi_test.cmulti_dec_advanced() 完整演示了这个模式。

7. MppDecCfg / MppEncCfg —— 配置接口

老式接口是一堆 mpi->control(ctx, MPP_XXX_SET_YYY, value),每个参数一个 control 码,扩展性差。新接口用一个配置对象 + 字符串路径

MppDecCfg cfg = NULL;
mpp_dec_cfg_init(&cfg);
mpi->control(ctx, MPP_DEC_GET_CFG, cfg);                   // 拿默认值
mpp_dec_cfg_set_u32(cfg, "base:split_parse", 1);           // 改一项
mpi->control(ctx, MPP_DEC_SET_CFG, cfg);                   // 应用
mpp_dec_cfg_deinit(cfg);

字符串路径用 : 分组,常见前缀:

  • 解码 cfg:base: split: cmn:
  • 编码 cfg:prep:(preprocess)rc:(rate control)codec: h264: h265: jpeg: vp8: split: hw: tune:

可以查 cfg/enc/base_h264_1920x1080.json 看完整字段列表(08-配置文件 有详解)。

8. split_parse / 帧切分

解码器要求”每次喂进来的 packet 是完整一帧”。但实际拿到的码流文件常常是连续字节流(特别是 raw H.264,没有容器格式)。怎么切?

  • 自己切:用第三方解析器找 NAL 起始码、frame boundary,每次 set length 为这一帧的字节数。
  • 让 MPP 切:开 split_parse=1,你随便扔字节进去,MPP 内部按起始码切。

测试代码全部走第二条路:

mpp_dec_cfg_set_u32(cfg, "base:split_parse", need_split);   // need_split = 1
mpi->control(ctx, MPP_DEC_SET_CFG, cfg);

JPEG 是例外——JPEG 每个文件就是一帧,不存在”切”的问题。mpi_dec_test.ccmd->simple 标志在非 JPEG 时为 1,走 split 路径;JPEG 时走 advanced 路径,每次喂整个 JPEG 文件 buffer。

9. 错误码 MPP_RET

typedef enum {
   MPP_OK              = 0,
   MPP_NOK             = -1,
   MPP_ERR_UNKNOW      = -2,
   MPP_ERR_NULL_PTR    = -3,
   MPP_ERR_MALLOC      = -4,
  ...
   MPP_ERR_TIMEOUT     = -22,   // poll/get 超时(非阻塞模式下常见)
  ...
} MPP_RET;

常见处理模式:

ret = mpi->decode_get_frame(ctx, &frame);
if (MPP_ERR_TIMEOUT == ret) {
   // 队列空,稍等
   msleep(1);
   continue;
}
if (ret) {
   mpp_err("decode_get_frame failed ret %d\n", ret);
   break;
}

10. 阻塞模式 MppPollType

mpi->control(ctx, MPP_SET_INPUT_TIMEOUT, &val) 设置 put_packet/encode_put_frame 的阻塞行为:

typedef enum {
   MPP_POLL_BLOCK      = -1,   // 一直等到队列有空位
   MPP_POLL_NON_BLOCK  =  0,   // 立刻返回,队列满就 MPP_ERR_TIMEOUT
   // 正整数 N 表示等 N ms
} MppPollType;
  • mpi_dec_test:默认非阻塞,put 失败就 sleep 1ms 重试。
  • mpi_dec_mt_test:设了 OUTPUT_TIMEOUT 为 BLOCK,让 get_frame 一直等,省掉外面的轮询。
  • mpi_enc_test:设了 OUTPUT_TIMEOUT 为 BLOCK,与上同。

选哪个? 想要低 CPU 占用 → block;想要严格控制流水线节奏 → non_block + 自己 sleep。

11. 日志和环境变量

  • mpp_log / mpp_err —— 标准日志宏,自动加 MODULE_TAG 前缀
  • mpp_log_q(quiet, ...) —— quiet 模式下静默
  • mpp_env_set_u32 / mpp_env_get_u32 —— 进程内”伪环境变量”,运行时改 MPP 行为
    • mpp_env_get_u32("dbrh_en", &val, 0) —— mpi_rc2_test 用这个开 debreath
    • mpp_env_get_u32("fbc_dec_en", &val, 0) —— 开 FBC 解码输出
    • mpp_env_get_u32("fast_en", &val, 0) —— 开解码 fast mode

也可以从真正的 OS 环境变量读,写 setenv MPP_SHOW_HISTORY 1 同样有效。详细列表查 osal/mpp_env.c

12. 一图速记

┌─────────────────────────────────────────────────────────────┐
│ 应用代码                                                     │
│                                                               │
│ ┌──────┐   put   ┌─────────┐   get   ┌──────┐             │
│ │packet│─────────▶│ ctx   │◀─────────│packet│             │
│ └──────┘         │ (mpi->) │         └──────┘             │
│ ┌──────┐ pre-set │         │ decoded ┌──────┐             │
│ │frame │─────────▶│         │─────────▶│frame │             │
│ └──────┘         └─────────┘         └──────┘             │
│     ▲                 │                   ▲                 │
│     │                 ▼                   │                 │
│     │             MppBufferGroup           │                 │
│     │                 │                   │                 │
│     └─────[MppBuffer]──┴────[MppBuffer]─────┘                 │
│                                                               │
│ control(): get/set cfg、reset、设 timeout、设 buffer group │
└─────────────────────────────────────────────────────────────┘

延伸阅读

  • 项目 doc/Rockchip_Developer_Guide_MPP_CN.md:官方开发者文档(更细)
  • doc/design/3.mpp_buffer.txt:MppBuffer 设计文档
  • doc/design/4.mpp_task.txt:MppTask 设计文档
  • inc/rk_mpi.h:所有 API 的声明(最权威)
© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像-天煜博客
欢迎您留下宝贵的见解!
提交
头像-天煜博客

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容