![图片[1]-瑞芯微开发笔记 · MPP 篇(二)MPP 核心概念-天煜博客](https://blog.itianyu.cn/wp-content/uploads/2026/05/20260509105314299-ChatGPT-Image-2026年5月9日-10_52_42-1024x757.png)
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/9、MPP_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 data、length 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/分层编码的层 IDKEY_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.c 的 multi_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.c 里 cmd->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用这个开 debreathmpp_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 的声明(最权威)
本网站尊重知识产权,如有侵权,请及时联系我们删除。
本站所有原创内容仅用于学习和交流目的,未经作者和本站授权不得进行商业使用或盈利行为。











暂无评论内容