前言
这篇文章主要讲解 RK 平台上的零拷贝到底是怎么回事。在云栈社区,经常有开发者被这个问题绕得晕头转向,今天我们就从源码到架构,把它彻底拆开讲清楚。
一、为什么视频链路特别需要零拷贝
视频帧是大块、连续、高频的数据。以 NV12 为例,一帧占用空间近似为:
frame_size = stride × aligned_height × 3 / 2
1920×1080、30 帧每秒的理想紧凑 NV12 数据约为 89 MiB/s;2560×1440、30 帧每秒约为 158 MiB/s。
若采集后先复制到应用缓冲区,再复制到编码器输入缓冲区,除了采集 DMA 和编码器读取本身,还会额外产生两次 CPU 内存读写,带来以下问题:
- DDR 带宽被重复占用;
- CPU 时间消耗在
memcpy(),而不是业务处理;
- Cache 被大块图像反复冲刷;
- 流水线延迟增加;
- 多路摄像头、4K 或高帧率场景更容易出现掉帧和抖动。
零拷贝的目标不是让数据完全不移动。Sensor 数据仍要经硬件写入内存,编码器仍要读取内存。真正需要消除的是模块交接处的 CPU 大块复制:
传统路径:
采集 DMA
↓
缓冲区 A
↓ CPU memcpy
缓冲区 B
↓
编码器读取
共享缓冲路径:
采集 DMA
↓
同一块 DMA-BUF
↓
编码器直接读取
换句话说,零拷贝的核心不是“没有 DMA”,而是:
同一组物理页在多个硬件模块之间共享,模块交接时只传递句柄、元数据和所有权,不搬运整帧像素。
二、Rockchip 媒体栈中的整体位置
《MPP 开发参考》第 5 页的系统框架图把软件栈划分为应用、MPI、MPP 编解码与视频处理模块、HAL/OSAL、内核驱动和硬件加速器。MPP 的作用是屏蔽芯片差异,并通过统一接口驱动 VPU、RKVDEC、RKVENC 等硬件。
在摄像头到编码器的场景中,可以把数据面理解为:
┌──────────────────────────────────────────────────────────────┐
│ 应用层 │
│ V4L2 队列管理 DMA-BUF fd 管理 MPP 编码控制 │
└───────────────┬───────────────────┬──────────────────────────┘
│ ioctl/QBUF/DQBUF │ MppFrame/MppBuffer
▼ ▼
┌──────────────────────────┐ ┌───────────────────────────────┐
│ V4L2 Core + Videobuf2 │ │ MPP / MPI / HAL │
│ video_device │ │ MppCtx / MppApi │
│ vb2_queue │ │ MppFrame / MppBuffer │
└───────────────┬──────────┘ └───────────────┬───────────────┘
│ │
└──────── DMA-BUF 共享 ────────┘
│
┌────────────▼────────────┐
│ DMA-BUF / IOMMU / DMA │
│ dma_buf_attachment │
│ sg_table / DMA address │
└───────┬───────────┬─────┘
│ │
┌─────▼─────┐ ┌───▼────────┐
│ RKCIF/ISP │ │ RKVENC/VPU │
│ 写图像帧 │ │ 读图像帧 │
└───────────┘ └────────────┘
对于 RV1126B 一类平台,实际摄像头链路可能包含:
Sensor
↓
MIPI D-PHY / CSI-2
↓
RKCIF
↓
RKISP
↓
ISPP / VPSS / RGA
↓
MPP Encoder
其中各框架的职责并不相同:
| 模块 |
主要职责 |
| Media Controller |
描述 entity、pad、link 和媒体拓扑 |
| V4L2 |
暴露视频节点、格式和 IOCTL |
| Videobuf2 |
管理缓冲区申请、排队、完成和出队 |
| DMA-BUF |
跨模块共享同一块后备存储 |
| IOMMU |
为不同设备建立可访问的 DMA 地址映射 |
| RKCIF/RKISP |
通过 DMA 写入图像 |
| MPP |
将共享图像封装成编解码任务并调度硬件 |
| RKVENC/VPU |
从共享图像缓冲区读取并编码 |
源码中可以看到,RKCIF、RKISP、ISPP、VPSS、RGA 的多个队列都开放了 VB2_DMABUF,部分节点同时实现 VIDIOC_EXPBUF。
例如:
cif/capture.c:8921
isp/capture_v30.c:1498
ispp/stream.c:1178
vpss/stream_v20.c:2165
rga/rga.c:103-117
这些位置都建立了 DMA-BUF 队列能力。
另外:
ispp/stream.c:1481
vpss/stream_v20.c:2985
等位置接入了 vb2_ioctl_expbuf。
这说明 DMA-BUF 不是某一个驱动的私有技巧,而是整个 Rockchip 媒体数据面共同使用的内存互操作机制。
三、零拷贝究竟“零”的是什么
1. 不复制像素数据
多个模块引用同一组内存页,交接时不调用 memcpy() 搬运整帧图像。
2. 仍然会复制少量元数据
下面这些信息仍然需要在不同层之间传递:
v4l2_buffer
plane 长度
buffer index
时间戳
帧序号
MppFrame 属性
MppPacket 属性
这些结构通常只有几十到几百字节,与数 MiB 的图像相比可以忽略。
3. 仍然会建立 DMA 映射
同一个 DMA-BUF 到达不同设备时,需要执行:
dma_buf_attach()
↓
dma_buf_map_attachment()
↓
得到该设备可访问的 sg_table
↓
得到 DMA address
映射不是像素复制,而是把同一组内存页转换成目标设备可使用的 DMA 地址视图。
4. 仍然可能进行 Cache 同步
CPU 和设备之间切换访问权时,非一致性平台可能需要执行:
clean cache
invalidate cache
dma_sync_sgtable_for_device()
dma_sync_sgtable_for_cpu()
Cache 同步不等于整帧复制,但频繁让 CPU 访问图像仍会增加开销。
5. 格式转换可能产生新的缓冲区
若 ISP 输出 NV12,而后级只接受另一种格式,就可能通过 RGA、VPSS 或其他硬件写入第二块缓冲区。
这时应区分两种情况:
共享缓冲零拷贝:
生产者和消费者直接使用同一块 DMA-BUF
硬件无 CPU 拷贝:
硬件 A 读取缓冲区 1
硬件 B 写入缓冲区 2
CPU 不执行整帧搬运
前者内存流量更低;后者虽然会产生一次硬件读写,但仍然比 CPU memcpy() 更适合图像处理。
四、DMA-BUF 的对象链:fd 只是入口
应用看到的是一个整数 fd,但内核和硬件看到的是一组对象:
DMA-BUF fd
│
│ dma_buf_get(fd)
▼
struct dma_buf
│
│ dma_buf_attach(dbuf, device)
▼
struct dma_buf_attachment
│
│ dma_buf_map_attachment()
▼
struct sg_table
│
│ sg_dma_address()
▼
设备可编程的 DMA address
各对象的职责如下:
| 对象 |
作用 |
| DMA-BUF fd |
在进程和不同 API 之间传递共享内存句柄 |
struct dma_buf |
表示一块可共享的后备存储,并维护引用计数 |
dma_buf_attachment |
表示某个设备对 DMA-BUF 的一次附着关系 |
sg_table |
描述后备页及其 DMA 映射结果 |
| DMA address |
最终写入 CIF、ISP、RGA、VPU 等硬件寄存器的地址 |
同一个 struct dma_buf 可以被多个设备分别 attach。
例如同一块图像内存可能同时被以下设备使用:
RKCIF:DMA 写入
RKISP:DMA 读取或写入
RGA:DMA 读取并转换
RKVENC:DMA 读取并编码
Display:扫描输出
每个设备可能位于不同的 IOMMU 域,因此它们看到的 DMA 地址不一定相同,但底层后备页仍然是同一组。
五、两种主流共享缓冲架构
5.1 采集端分配,再导出
流程如下:
V4L2 REQBUFS(MMAP)
↓
VB2 分配缓冲区
↓
VIDIOC_EXPBUF 导出 DMA-BUF fd
↓
RKCIF/ISP 写入
↓
DQBUF 得到完成帧
↓
MPP 导入同一个 fd
↓
编码完成
↓
重新 QBUF
这种模式改造成本较低,适合已有 V4L2 MMAP 采集程序。
需要注意:
VIDIOC_EXPBUF 导出的仍然是 VB2 原来的缓冲区,并没有创建第二份像素数据。
5.2 外部统一分配,再分别导入
流程如下:
DMA-HEAP / DRM / MPP 分配共享缓冲池
↓
得到一组 DMA-BUF fd
↓
V4L2 以 V4L2_MEMORY_DMABUF 方式 QBUF
↓
MPP 同时导入这些 fd
↓
同一组缓冲区在采集与编码之间轮转
这种模式便于统一控制:
缓冲区数量
内存类型
图像格式
stride
plane 布局
生命周期
它更接近完整的端到端共享缓冲架构。
《MPP 开发参考》第 9 页的外部导入示意图把缓冲池画成 external MppBufferGroup:外部分配的 dmabuf/ion/drm 句柄被提交到缓冲组,再交给解码器循环使用;该模式更容易连接显示系统。
六、MPP 侧的内存抽象
MPP 接口中最关键的几类对象是:
MppBuffer
硬件可访问内存的封装
MppFrame
图像宽高、stride、格式、时间戳与 MppBuffer 的组合
MppPacket
一维码流数据的封装
MppTask
高级输入输出任务容器
《MPP 开发参考》第 7 页明确区分 MppMem 与 MppBuffer:
MppMem
普通 C 内存封装
MppBuffer
面向硬件可访问的 DMA-BUF 内存
MppFrame 和 MppPacket 可以引用 MppBuffer。
MppBuffer 保存的关键属性包括:
ptr
size
fd
引用计数
分配器类型
并负责分配、释放和引用管理。
这意味着 MPP 并不要求图像一定由 MPP 自己分配。
只要外部内存能够被封装为 MppBuffer,编码器就可以直接读取。
七、MppFrame 不只是一个地址
共享 fd 只是第一步,编码器还必须知道图像布局。
MppFrame 至少要正确描述:
width
有效图像宽度
height
有效图像高度
hor_stride
相邻两行之间的字节跨度
ver_stride
图像分量布局使用的对齐高度
format
NV12、NV21、YUV420P 等内存格式
buffer
对应的 MppBuffer
《MPP 开发参考》第 11 页对 width、height、hor_stride、ver_stride 作了明确区分。
编码器输入空间还必须满足硬件的对齐和额外填充要求,不能只按可见宽高计算。
例如:
有效图像:
width = 1920
height = 1080
实际内存布局可能是:
hor_stride = 1920
ver_stride = 1088
那么 NV12 的 UV 起始地址应按照:
UV offset = hor_stride × ver_stride
计算,而不是:
UV offset = width × height
若两端对 stride 的理解不一致,常见现象包括:
绿边
紫边
UV 错位
斜纹
花屏
DMA 越界
编码器异常
八、编码输入与码流输出不是同一件事
《MPP 开发参考》第 25 页指出,编码器输入图像很大,CPU 分配地址通常需要先复制到 MppBuffer;dmabuf/ion/drm 形式更适合直接编码,可把额外开销降到最低。
这意味着:
V4L2 DMA-BUF
↓
MppBuffer
↓
MppFrame
↓
MPP Encoder
可以形成原始图像输入零拷贝。
但简单的 encode_get_packet() 接口没有外部输出缓冲配置入口,因此码流输出仍会有一次复制;若连码流输出也要求共享缓冲,需要使用 enqueue/dequeue 与 MppTask 任务接口。
因此:
“V4L2 采集到 MPP 编码零拷贝”通常首先指原始图像输入不复制,并不自动代表编码后的 H.264/H.265 码流也没有复制。
九、RKCIF 源码:队列如何具备 DMA-BUF 能力
9.1 vb2_queue 明确开放 MMAP 和 DMA-BUF
源码位置:
media/platform/rockchip/cif/capture.c:8914-8938
核心代码:
static int rkcif_init_vb2_queue(struct vb2_queue *q,
struct rkcif_stream *stream,
enum v4l2_buf_type buf_type)
{
struct rkcif_hw *hw_dev = stream->cifdev->hw_dev;
q->type = buf_type;
q->io_modes = VB2_MMAP | VB2_DMABUF;
q->drv_priv = stream;
q->ops = &rkcif_vb2_ops;
q->mem_ops = hw_dev->mem_ops;
q->buf_struct_size = sizeof(struct rkcif_buffer);
if (stream->cifdev->is_use_dummybuf)
q->min_buffers_needed = 1;
else
q->min_buffers_needed = CIF_REQ_BUFS_MIN;
q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
q->lock = &stream->vnode.vlock;
q->dev = hw_dev->dev;
q->allow_cache_hints = 1;
q->bidirectional = 1;
q->gfp_flags = GFP_DMA32;
if (hw_dev->is_dma_contig)
q->dma_attrs = DMA_ATTR_FORCE_CONTIGUOUS;
return vb2_queue_init(q);
}
这段代码建立了零拷贝的基础条件。
VB2_MMAP
表示缓冲区由 VB2 内存后端分配。
这些缓冲区可以:
mmap 到进程地址空间
通过 VIDIOC_EXPBUF 导出 DMA-BUF
直接交给采集 DMA
VB2_DMABUF
表示队列允许导入外部 DMA-BUF。
在 QBUF 时,plane 可以携带 fd:
buf.memory = V4L2_MEMORY_DMABUF;
planes[i].m.fd = dmabuf_fd;
q->mem_ops
真正的内存操作由:
q->mem_ops = hw_dev->mem_ops;
决定。
包括:
alloc
put
mmap
prepare
finish
get_dmabuf
attach_dmabuf
map_dmabuf
unmap_dmabuf
detach_dmabuf
cookie
q->dev
q->dev = hw_dev->dev;
决定 DMA-BUF 应附着到哪个硬件设备。
DMA_ATTR_FORCE_CONTIGUOUS
当硬件或内存拓扑要求连续内存时:
q->dma_attrs = DMA_ATTR_FORCE_CONTIGUOUS;
会让内存后端走连续分配路径。
需要特别注意:
io_modes 只声明队列允许哪种 I/O 模式,真正决定能否导入和导出的是 mem_ops 是否实现相应回调。
9.2 RKCIF 的 VB2 回调表
源码位置:
media/platform/rockchip/cif/capture.c:8905-8912
static struct vb2_ops rkcif_vb2_ops = {
.queue_setup = rkcif_queue_setup,
.buf_queue = rkcif_buf_queue,
.wait_prepare = vb2_ops_wait_prepare,
.wait_finish = vb2_ops_wait_finish,
.stop_streaming = rkcif_stop_streaming,
.start_streaming = rkcif_start_streaming,
};
各回调的职责为:
| 回调 |
作用 |
queue_setup |
确认 plane 数量和每个 plane 的最小容量 |
buf_queue |
将通用 VB2 buffer 转成 RKCIF 可使用的 DMA 地址 |
start_streaming |
启动硬件采集 |
stop_streaming |
停止硬件并回收缓冲区 |
wait_prepare |
进入阻塞等待前释放队列锁 |
wait_finish |
等待结束后重新获取队列锁 |
其中与零拷贝关系最密切的是:
queue_setup
buf_queue
前者保证内存布局正确,后者把共享内存真正交给硬件。
十、IOCTL 表如何接入 VB2 通用实现
源码位置:
media/platform/rockchip/cif/capture.c:10495-10516
static const struct v4l2_ioctl_ops rkcif_v4l2_ioctl_ops = {
.vidioc_reqbufs = vb2_ioctl_reqbufs,
.vidioc_querybuf = vb2_ioctl_querybuf,
.vidioc_create_bufs = vb2_ioctl_create_bufs,
.vidioc_qbuf = vb2_ioctl_qbuf,
.vidioc_expbuf = vb2_ioctl_expbuf,
.vidioc_dqbuf = rkcif_dqbuf,
.vidioc_prepare_buf = vb2_ioctl_prepare_buf,
.vidioc_streamon = vb2_ioctl_streamon,
.vidioc_streamoff = vb2_ioctl_streamoff,
.vidioc_enum_input = rkcif_enum_input,
.vidioc_try_fmt_vid_cap_mplane = rkcif_try_fmt_vid_cap_mplane,
.vidioc_enum_fmt_vid_cap = rkcif_enum_fmt_vid_cap_mplane,
.vidioc_s_fmt_vid_cap_mplane = rkcif_s_fmt_vid_cap_mplane,
.vidioc_g_fmt_vid_cap_mplane = rkcif_g_fmt_vid_cap_mplane,
.vidioc_querycap = rkcif_querycap,
};
这张回调表说明 RKCIF 没有重新实现一套缓冲区框架,而是把以下命令交给 VB2:
REQBUFS
QUERYBUF
CREATE_BUFS
QBUF
EXPBUF
PREPARE_BUF
STREAMON
STREAMOFF
RKCIF 主要负责:
格式约束
硬件地址获取
硬件启动
硬件停止
中断处理
帧完成
这也是 Linux 媒体驱动的典型分层方式:
V4L2 Core
负责 IOCTL 分发
Videobuf2
负责通用缓冲区状态机
RKCIF
负责平台硬件
十一、缓冲区大小:零拷贝首先要求布局一致
源码位置:
media/platform/rockchip/cif/capture.c:5693-5743
static int rkcif_queue_setup(struct vb2_queue *queue,
unsigned int *num_buffers,
unsigned int *num_planes,
unsigned int sizes[],
struct device *alloc_ctxs[])
{
struct rkcif_stream *stream = queue->drv_priv;
struct rkcif_device *dev = stream->cifdev;
const struct v4l2_pix_format_mplane *pixm;
const struct cif_output_fmt *cif_fmt;
const struct cif_input_fmt *in_fmt;
u32 i, height;
pixm = &stream->pixm;
cif_fmt = stream->cif_fmt_out;
in_fmt = stream->cif_fmt_in;
*num_planes = cif_fmt->mplanes;
if (stream->crop_enable)
height = stream->crop[CROP_SRC_ACT].height;
else
height = pixm->height;
if (rkcif_is_extending_line_for_height(dev, stream, in_fmt) &&
stream->extend_line.is_extended) {
height = stream->extend_line.pixm.height;
pixm = &stream->extend_line.pixm;
}
for (i = 0; i < cif_fmt->mplanes; i++) {
const struct v4l2_plane_pix_format *plane_fmt;
int h;
h = round_up(height,
MEMORY_ALIGN_ROUND_UP_HEIGHT);
plane_fmt = &pixm->plane_fmt[i];
sizes[i] = plane_fmt->sizeimage / height * h;
}
stream->total_buf_num = *num_buffers;
return 0;
}
这段函数没有搬运图像,但它决定后续共享是否安全。
11.1 mplanes
*num_planes = cif_fmt->mplanes;
mplanes 表示一个 V4L2 buffer 包含多少块独立内存。
例如 NV12 可能采用:
1 个 memory plane:
Y 和 UV 位于同一个 fd 中
也可能采用:
2 个 memory plane:
Y 和 UV 分别位于不同 fd 中
11.2 sizeimage
sizes[i] = plane_fmt->sizeimage / height * h;
sizeimage 表示该 plane 所需的最低容量。
驱动还会根据对齐后的高度重新计算真实大小。
11.3 对齐高度
h = round_up(height,
MEMORY_ALIGN_ROUND_UP_HEIGHT);
硬件可能要求:
1080 对齐到 1088
2160 对齐到 2176
具体取决于芯片和格式。
11.4 裁剪和扩展行
源码还考虑:
crop_enable
extend_line
HDMI 输入
芯片版本
这些条件都可能改变实际分配大小。
因此,若 MPP 只按:
width × height × 3 / 2
理解缓冲,而 RKCIF 实际按对齐后的 stride 和 height 写入,编码器就可能:
读错 UV 地址
读取未初始化区域
越过 DMA-BUF 边界
产生绿边或花屏
零拷贝减少了中间复制,也意味着上下游必须对同一块内存布局达成完全一致。
十二、从 MMAP 缓冲导出 DMA-BUF
12.1 完整调用链
VIDIOC_EXPBUF
↓
rkcif_v4l2_ioctl_ops.vidioc_expbuf
↓
vb2_ioctl_expbuf()
↓
vb2_expbuf()
↓
vb2_core_expbuf()
↓
q->mem_ops->get_dmabuf()
↓
dma_buf_export()
↓
dma_buf_fd()
12.2 vb2_ioctl_expbuf()
源码位置:
media/common/videobuf2/videobuf2-v4l2.c:1098-1106
int vb2_ioctl_expbuf(struct file *file, void *priv,
struct v4l2_exportbuffer *p)
{
struct video_device *vdev = video_devdata(file);
if (vb2_queue_is_busy(vdev->queue, file))
return -EBUSY;
return vb2_expbuf(vdev->queue, p);
}
这个函数先从文件对象中取得:
struct video_device *vdev
再找到:
vdev->queue
最终进入 VB2。
12.3 vb2_expbuf()
源码位置:
media/common/videobuf2/videobuf2-v4l2.c:880-885
int vb2_expbuf(struct vb2_queue *q,
struct v4l2_exportbuffer *eb)
{
return vb2_core_expbuf(q,
&eb->fd,
eb->type,
eb->index,
eb->plane,
eb->flags);
}
这一层负责把 V4L2 结构转换成 VB2 Core 参数。
12.4 vb2_core_expbuf()
源码位置:
media/common/videobuf2/videobuf2-core.c:2238-2308
int vb2_core_expbuf(struct vb2_queue *q,
int *fd,
unsigned int type,
unsigned int index,
unsigned int plane,
unsigned int flags)
{
struct vb2_buffer *vb = NULL;
struct vb2_plane *vb_plane;
struct dma_buf *dbuf;
int ret;
if (q->memory != VB2_MEMORY_MMAP)
return -EINVAL;
if (!q->mem_ops->get_dmabuf)
return -EINVAL;
if (type != q->type)
return -EINVAL;
if (index >= q->num_buffers)
return -EINVAL;
vb = q->bufs[index];
if (plane >= vb->num_planes)
return -EINVAL;
vb_plane = &vb->planes[plane];
dbuf = call_ptr_memop(get_dmabuf,
vb,
vb_plane->mem_priv,
flags & O_ACCMODE);
if (IS_ERR_OR_NULL(dbuf))
return -EINVAL;
ret = dma_buf_fd(dbuf,
flags & ~O_ACCMODE);
if (ret < 0) {
dma_buf_put(dbuf);
return ret;
}
*fd = ret;
return 0;
}
几个关键结论:
EXPBUF 只导出 MMAP 队列
if (q->memory != VB2_MEMORY_MMAP)
return -EINVAL;
这意味着典型流程为:
REQBUFS(memory = MMAP)
↓
QUERYBUF
↓
EXPBUF
内存后端必须实现 get_dmabuf
if (!q->mem_ops->get_dmabuf)
return -EINVAL;
只声明 VB2_MMAP 并不足以导出。
导出时没有复制
dbuf = call_ptr_memop(get_dmabuf, ...);
这里从原 plane 的:
vb_plane->mem_priv
取得 DMA-BUF。
随后:
ret = dma_buf_fd(dbuf, ...);
只是把 struct dma_buf 安装到 fd 表,并没有创建第二块图像内存。
十三、Rockchip 内存后端如何导出 DMA-BUF
源码位置:
media/common/videobuf2/videobuf2-cma-sg.c:590-625
13.1 DMA-BUF 操作表
static const struct dma_buf_ops vb2_cma_sg_dmabuf_ops = {
.attach = vb2_cma_sg_dmabuf_ops_attach,
.detach = vb2_cma_sg_dmabuf_ops_detach,
.map_dma_buf = vb2_cma_sg_dmabuf_ops_map,
.unmap_dma_buf = vb2_cma_sg_dmabuf_ops_unmap,
.begin_cpu_access = vb2_cma_sg_dmabuf_ops_begin_cpu_access,
.end_cpu_access = vb2_cma_sg_dmabuf_ops_end_cpu_access,
.vmap = vb2_cma_sg_dmabuf_ops_vmap,
.mmap = vb2_cma_sg_dmabuf_ops_mmap,
.release = vb2_cma_sg_dmabuf_ops_release,
};
这张表定义了导出后其他模块可以对这块内存执行的操作:
attach
detach
map
unmap
CPU access
vmap
mmap
release
13.2 vb2_cma_sg_get_dmabuf()
static struct dma_buf *
vb2_cma_sg_get_dmabuf(struct vb2_buffer *vb,
void *buf_priv,
unsigned long flags)
{
struct vb2_cma_sg_buf *buf = buf_priv;
struct dma_buf *dbuf;
DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
exp_info.ops = &vb2_cma_sg_dmabuf_ops;
exp_info.size = buf->size;
exp_info.flags = flags;
exp_info.priv = buf;
if (WARN_ON(!buf->dma_sgt))
return NULL;
dbuf = dma_buf_export(&exp_info);
if (IS_ERR(dbuf))
return NULL;
refcount_inc(&buf->refcount);
return dbuf;
}
核心是:
exp_info.priv = buf;
这里把原来的:
struct vb2_cma_sg_buf
放进 DMA-BUF 私有数据。
这意味着:
VB2 buffer
↓
vb2_cma_sg_buf
↓
dma_buf_export
↓
DMA-BUF fd
导出后的 fd 与原 VB2 缓冲共享同一个后备对象。
13.3 引用计数保护生命周期
refcount_inc(&buf->refcount);
DMA-BUF 导出后会额外持有一次引用。
即使视频节点释放了部分对象,只要 DMA-BUF 仍然被其他模块持有,后备内存就不能提前释放。
十四、外部 DMA-BUF 如何导入 RKCIF
当 QBUF 使用:
V4L2_MEMORY_DMABUF
时,plane 中携带的是 fd。
完整调用链如下:
VIDIOC_QBUF
memory = V4L2_MEMORY_DMABUF
↓
vb2_ioctl_qbuf()
↓
vb2_core_qbuf()
↓
__prepare_dmabuf()
↓
dma_buf_get(fd)
↓
mem_ops->attach_dmabuf()
↓
dma_buf_attach()
↓
mem_ops->map_dmabuf()
↓
dma_buf_map_attachment()
↓
sg_table
↓
RKCIF buf_queue()
14.1 __prepare_dmabuf()
源码位置:
media/common/videobuf2/videobuf2-core.c:1261-1351
关键逻辑如下:
static int __prepare_dmabuf(struct vb2_buffer *vb)
{
struct vb2_plane planes[VB2_MAX_PLANES];
struct vb2_queue *q = vb->vb2_queue;
void *mem_priv;
unsigned int plane;
int ret;
memset(planes, 0,
sizeof(planes[0]) * vb->num_planes);
ret = call_bufop(q,
fill_vb2_buffer,
vb,
planes);
if (ret)
return ret;
for (plane = 0;
plane < vb->num_planes;
++plane) {
struct dma_buf *dbuf;
dbuf = dma_buf_get(
planes[plane].m.fd);
if (IS_ERR_OR_NULL(dbuf))
return -EINVAL;
if (planes[plane].length == 0)
planes[plane].length =
dbuf->size;
if (planes[plane].length <
vb->planes[plane].min_length) {
dma_buf_put(dbuf);
return -EINVAL;
}
mem_priv =
call_ptr_memop(
attach_dmabuf,
vb,
q->alloc_devs[plane] ?
q->alloc_devs[plane] :
q->dev,
dbuf,
planes[plane].length);
if (IS_ERR(mem_priv)) {
dma_buf_put(dbuf);
return PTR_ERR(mem_priv);
}
vb->planes[plane].dbuf =
dbuf;
vb->planes[plane].mem_priv =
mem_priv;
}
for (plane = 0;
plane < vb->num_planes;
++plane) {
ret = call_memop(
vb,
map_dmabuf,
vb->planes[plane].mem_priv);
if (ret)
return ret;
vb->planes[plane].dbuf_mapped = 1;
}
return 0;
}
上面的摘录为突出主线省略了完整源码中的错误回滚;实际实现会在后续 plane 失败时解除前面已经建立的映射和附着,并释放相应引用。
这段代码完成了五件事。
第一步:从 fd 取得 DMA-BUF
dbuf = dma_buf_get(planes[plane].m.fd);
fd 在这里变成:
struct dma_buf *
第二步:校验缓冲区长度
if (planes[plane].length <
vb->planes[plane].min_length)
return -EINVAL;
如果外部 DMA-BUF 小于该 plane 的最低要求,驱动会拒绝 QBUF。
这是防止 DMA 越界的重要保护。
第三步:attach 到 RKCIF 设备
mem_priv = call_ptr_memop(
attach_dmabuf,
vb,
q->dev,
dbuf,
planes[plane].length);
这一步建立:
DMA-BUF
↕
RKCIF device
之间的附着关系。
第四步:建立 DMA 映射
ret = call_memop(
vb,
map_dmabuf,
vb->planes[plane].mem_priv);
这一步得到目标设备视角下的 sg_table。
第五步:保存映射状态
vb->planes[plane].dbuf_mapped = 1;
整个过程没有申请第二块图像内存,也没有复制像素。
十五、Rockchip memops 的 attach 与 map
源码位置:
media/common/videobuf2/videobuf2-cma-sg.c:632-728
15.1 vb2_cma_sg_attach_dmabuf()
static void *
vb2_cma_sg_attach_dmabuf(struct vb2_buffer *vb,
struct device *dev,
struct dma_buf *dbuf,
unsigned long size)
{
struct vb2_cma_sg_buf *buf;
struct dma_buf_attachment *dba;
if (WARN_ON(!dev))
return ERR_PTR(-EINVAL);
if (dbuf->size < size)
return ERR_PTR(-EFAULT);
buf = kzalloc(sizeof(*buf),
GFP_KERNEL);
if (!buf)
return ERR_PTR(-ENOMEM);
buf->dev = dev;
dba = dma_buf_attach(dbuf,
buf->dev);
if (IS_ERR(dba)) {
kfree(buf);
return dba;
}
buf->dma_dir =
vb->vb2_queue->dma_dir;
buf->size = size;
buf->db_attach = dba;
buf->vb = vb;
return buf;
}
关键调用是:
dma_buf_attach(dbuf, dev);
它表示:
这块共享内存将被当前 RKCIF 设备访问。
返回的:
struct dma_buf_attachment
保存了该设备和 DMA-BUF 之间的关系。
15.2 vb2_cma_sg_map_dmabuf()
static int
vb2_cma_sg_map_dmabuf(void *mem_priv)
{
struct vb2_cma_sg_buf *buf =
mem_priv;
struct sg_table *sgt;
if (WARN_ON(!buf->db_attach))
return -EINVAL;
if (WARN_ON(buf->dma_sgt))
return 0;
sgt = dma_buf_map_attachment(
buf->db_attach,
buf->dma_dir);
if (IS_ERR(sgt))
return -EINVAL;
buf->dma_sgt = sgt;
buf->vaddr = NULL;
return 0;
}
关键调用是:
dma_buf_map_attachment()
它获取当前设备可使用的 scatter-gather 映射。
映射完成后:
buf->dma_sgt = sgt;
后续 RKCIF 就可以从 sg_table 中取得 DMA 地址。
即使底层物理页不连续,只要内存后端和 IOMMU 能提供硬件可接受的 DMA 映射,RKCIF 就能直接访问。
十六、RKCIF buf_queue():把共享内存交给硬件
源码位置:
media/platform/rockchip/cif/capture.c:6174-6266
关键代码:
void rkcif_buf_queue(struct vb2_buffer *vb)
{
struct vb2_v4l2_buffer *vbuf =
to_vb2_v4l2_buffer(vb);
struct rkcif_buffer *cifbuf =
to_rkcif_buffer(vbuf);
struct vb2_queue *queue =
vb->vb2_queue;
struct rkcif_stream *stream =
queue->drv_priv;
const struct cif_output_fmt *fmt =
stream->cif_fmt_out;
struct rkcif_hw *hw_dev =
stream->cifdev->hw_dev;
unsigned long flags;
int i;
memset(cifbuf->buff_addr,
0,
sizeof(cifbuf->buff_addr));
for (i = 0;
i < fmt->mplanes;
i++) {
if (hw_dev->is_dma_sg_ops) {
struct sg_table *sgt;
sgt = vb2_dma_sg_plane_desc(
vb,
i);
cifbuf->buff_addr[i] =
sg_dma_address(
sgt->sgl);
} else {
cifbuf->buff_addr[i] =
vb2_dma_contig_plane_dma_addr(
vb,
i);
}
}
if (fmt->mplanes == 1) {
for (i = 0;
i < fmt->cplanes - 1;
i++) {
cifbuf->buff_addr[i + 1] =
cifbuf->buff_addr[i] +
stream->pixm
.plane_fmt[i]
.bytesperline *
stream->pixm.height;
}
}
spin_lock_irqsave(
&stream->vbq_lock,
flags);
list_add_tail(
&cifbuf->queue,
&stream->buf_head);
spin_unlock_irqrestore(
&stream->vbq_lock,
flags);
}
这是从通用 VB2 缓冲区进入 RKCIF 硬件缓冲区的关键边界。
16.1 SG 路径
sgt = vb2_dma_sg_plane_desc(vb, i);
cifbuf->buff_addr[i] =
sg_dma_address(sgt->sgl);
这里取得的是已经为 RKCIF 建立映射后的 DMA 地址。
16.2 连续内存路径
cifbuf->buff_addr[i] =
vb2_dma_contig_plane_dma_addr(vb, i);
若使用连续内存,则直接取得 contiguous DMA address。
16.3 单内存 plane、多颜色分量
if (fmt->mplanes == 1)
表示多个颜色分量位于同一块内存中。
例如单 plane NV12:
DMA base
↓
Y plane
↓ offset
UV plane
UV 地址通过:
bytesperline × height
计算。
16.4 加入硬件待处理队列
list_add_tail(&cifbuf->queue,
&stream->buf_head);
这里只是把缓冲对象挂入 RKCIF 队列。
没有:
新建图像缓冲
CPU memcpy
逐行复制
颜色转换
十七、源码中的调试 memset() 为什么不属于主链路
rkcif_buf_queue() 中还能看到:
if (rkcif_debug &&
addr &&
!hw_dev->iommu_en) {
memset(addr,
0,
pixm->plane_fmt[i].sizeimage);
if (queue->mem_ops->finish)
queue->mem_ops->finish(
vb->planes[i].mem_priv);
}
这段代码容易引起误解。
它只在以下条件同时成立时执行:
rkcif_debug 非零
能够取得 CPU 虚拟地址
IOMMU 未启用
它的作用是调试时预清缓冲区,便于观察硬件是否真正写入数据。
正常运行时并不是帧交接步骤。
因此,做性能评估时应确认:
rkcif_debug 是否关闭
是否存在整帧 memset
是否存在额外 CPU 访问
否则即使数据交接使用 DMA-BUF,调试逻辑仍可能引入较大的 CPU 和 DDR 开销。
十八、CIF 到 ISP:内核内部也在传递同一 DMA-BUF
rkcif_buf_queue() 中还有一段很有代表性的代码:
if ((stream->dma_en &
RKCIF_DMAEN_BY_ISP) &&
!cifbuf->dbuf) {
struct rkisp_rx_buf *dbufs;
dbufs = kzalloc(
sizeof(*dbufs),
GFP_ATOMIC);
if (dbufs) {
cifbuf->dbuf =
hw_dev->mem_ops->get_dmabuf(
vb,
vb->planes[0].mem_priv,
O_RDWR);
if (cifbuf->dbuf)
dbufs->dbuf =
cifbuf->dbuf;
list_add_tail(
&dbufs->list,
&stream->rx_buf_head_vicap);
}
}
这里没有把 CIF 数据复制给 ISP。
它做的是:
VB2 buffer
└── mem_priv
└── get_dmabuf()
└── struct dma_buf *
└── 放入 ISP 接收队列
跨驱动共享时可以有两种句柄形式:
进程边界:
DMA-BUF fd
内核内部:
struct dma_buf *
两种方式共享的都是同一后备存储,差别只在句柄所处层次。
这段代码直接说明,CIF 和 ISP 之间的交接可以围绕 DMA-BUF 对象完成,而不是通过 CPU 把图像从一块内存复制到另一块内存。
十九、内存后端:连续页、SG 与 IOMMU
RKCIF 的硬件初始化代码位于:
media/platform/rockchip/cif/hw.c:1862-1882
cif_hw->is_dma_sg_ops = true;
cif_hw->is_dma_contig = true;
cif_hw->iommu_en =
is_iommu_enable(dev);
ret = of_reserved_mem_device_init(dev);
if (ret) {
is_mem_reserved = false;
dev_info(dev,
"No reserved memory region "
"assign to CIF\n");
}
if (cif_hw->iommu_en &&
!is_mem_reserved)
cif_hw->is_dma_contig = false;
cif_hw->mem_ops =
&vb2_cma_sg_memops;
这段选择逻辑说明:
- RKCIF 统一使用 Rockchip 的
vb2_cma_sg_memops;
- 有保留内存或硬件要求时,可以请求连续内存;
- IOMMU 可用且没有绑定保留内存时,可以不强制物理连续;
- SG/IOMMU 路径负责提供设备可访问的 DMA 映射。
19.1 vb2_cma_sg_buf 保存了什么
源码位置:
media/common/videobuf2/videobuf2-cma-sg.c:22-48
struct vb2_cma_sg_buf {
struct device *dev;
void *vaddr;
struct page **pages;
struct frame_vector *vec;
int offset;
unsigned long dma_attrs;
enum dma_data_direction dma_dir;
struct sg_table sg_table;
struct sg_table *dma_sgt;
size_t size;
unsigned int num_pages;
refcount_t refcount;
struct vb2_vmarea_handler handler;
struct dma_buf_attachment *db_attach;
struct vb2_buffer *vb;
struct rk_dma_heap *contig_heap;
struct rk_dma_heap *heap;
};
其中最重要的是:
pages
后备物理页数组
sg_table
由页数组建立的 SG 表
dma_sgt
当前设备映射后的 SG 表
db_attach
DMA-BUF attachment
refcount
生命周期引用计数
vaddr
可选 CPU 虚拟地址
19.2 分配连续页或压缩页
源码位置:
media/common/videobuf2/videobuf2-cma-sg.c:174-248
static void *
vb2_cma_sg_alloc(struct vb2_buffer *vb,
struct device *dev,
unsigned long size)
{
struct vb2_cma_sg_buf *buf;
struct sg_table *sgt;
unsigned long dma_attrs;
int ret;
if (WARN_ON(!dev) ||
WARN_ON(!size))
return ERR_PTR(-EINVAL);
buf = kzalloc(sizeof(*buf),
GFP_KERNEL);
if (!buf)
return ERR_PTR(-ENOMEM);
dma_attrs =
vb->vb2_queue->dma_attrs;
buf->dma_attrs = dma_attrs;
buf->dma_dir =
vb->vb2_queue->dma_dir;
buf->size = size;
buf->num_pages =
size >> PAGE_SHIFT;
buf->dma_sgt =
&buf->sg_table;
buf->dev =
get_device(dev);
buf->pages =
kvcalloc(buf->num_pages,
sizeof(struct page *),
GFP_KERNEL);
if (!buf->pages)
goto fail;
if (dma_attrs &
DMA_ATTR_FORCE_CONTIGUOUS) {
ret =
vb2_cma_sg_alloc_contiguous(
buf);
} else {
ret =
vb2_cma_sg_alloc_compacted(
buf,
vb->vb2_queue->gfp_flags);
}
if (ret)
goto fail;
ret =
sg_alloc_table_from_pages(
buf->dma_sgt,
buf->pages,
buf->num_pages,
0,
size,
GFP_KERNEL);
if (ret)
goto fail;
sgt = &buf->sg_table;
if (dma_map_sgtable(
buf->dev,
sgt,
buf->dma_dir,
DMA_ATTR_SKIP_CPU_SYNC))
goto fail;
refcount_set(
&buf->refcount,
1);
return buf;
fail:
/* 完整源码中执行对应资源回滚 */
return ERR_PTR(-ENOMEM);
}
这段代码的核心流程是:
确定缓冲区大小
↓
分配 page 数组
↓
连续分配或非连续页分配
↓
由 pages 建立 sg_table
↓
dma_map_sgtable()
↓
得到设备可访问的 DMA 映射
无论后备页是连续还是分散,最终都被组织为 sg_table 并映射给设备。
因此:
零拷贝并不要求物理内存必然连续,它要求各硬件模块能够对同一后备存储建立有效 DMA 映射。
二十、Cache 同步:零拷贝不等于零维护
Rockchip 内存后端实现了 VB2 的 prepare 和 finish。
源码位置:
media/common/videobuf2/videobuf2-cma-sg.c:272-292
static void
vb2_cma_sg_prepare(void *buf_priv)
{
struct vb2_cma_sg_buf *buf =
buf_priv;
struct sg_table *sgt =
buf->dma_sgt;
if (buf->vb
->skip_cache_sync_on_prepare)
return;
dma_sync_sgtable_for_device(
buf->dev,
sgt,
buf->dma_dir);
}
static void
vb2_cma_sg_finish(void *buf_priv)
{
struct vb2_cma_sg_buf *buf =
buf_priv;
struct sg_table *sgt =
buf->dma_sgt;
if (buf->vb
->skip_cache_sync_on_finish)
return;
dma_sync_sgtable_for_cpu(
buf->dev,
sgt,
buf->dma_dir);
}
它们对应两个方向。
20.1 prepare
CPU
↓
设备
如果 CPU 修改过缓冲区,在设备读取或写入之前,需要确保设备看到正确内容。
20.2 finish
设备
↓
CPU
如果设备写过缓冲区,在 CPU 读取之前,需要确保 CPU Cache 中的数据有效。
纯摄像头采集到编码器的共享路径中,CPU 通常不需要访问像素。
因此应尽量避免:
对整帧执行 mmap 后读取
对整帧执行 memset
软件颜色转换
软件缩放
无意义的逐像素检查
只在调试、算法或软件叠加确实需要时才触碰图像,并正确遵守 CPU access 协议。
二十一、一帧完成:只改变状态,不复制数据
源码位置:
media/platform/rockchip/cif/capture.c:10518-10535
void rkcif_vb_done_oneframe(
struct rkcif_stream *stream,
struct vb2_v4l2_buffer *vb_done)
{
const struct cif_output_fmt *fmt =
stream->cif_fmt_out;
u32 i;
for (i = 0;
i < fmt->mplanes;
i++) {
vb2_set_plane_payload(
&vb_done->vb2_buf,
i,
stream->pixm
.plane_fmt[i]
.sizeimage);
}
vb2_buffer_done(
&vb_done->vb2_buf,
VB2_BUF_STATE_DONE);
atomic_dec(&stream->buf_cnt);
}
硬件完成中断到达后,驱动主要做两件事。
设置有效数据长度
vb2_set_plane_payload()
这会更新每个 plane 的:
bytesused
payload size
把状态改为 DONE
vb2_buffer_done(...,
VB2_BUF_STATE_DONE);
随后 DQBUF 取得的仍然是原来的:
buffer index
plane
DMA-BUF
后备页
帧完成路径没有生成新的图像副本。
真正变化的是所有权:
QUEUED
↓
由 RKCIF 占有
↓
硬件写入
↓
DONE
↓
可交给后级
二十二、Rockit 私有路径:直接接收 struct dma_buf *
源码中的:
media/platform/rockchip/cif/cif-rockit.c
展示了另一条更直接的共享路径。
其缓冲对象为:
struct rkcif_rockit_buffer {
struct vb2_buffer vb;
struct vb2_queue vb2_queue;
struct rkcif_buffer cif_buf;
struct dma_buf *dmabuf;
void *mpi_mem;
void *mpi_buf;
struct list_head queue;
int buf_id;
union {
u32 buff_addr;
void *vaddr;
};
};
这里同时保存:
VB2 buffer
RKCIF buffer
struct dma_buf *
MPI 相关句柄
DMA address
它是连接媒体驱动和 Rockchip 私有媒体接口的桥接对象。
22.1 attach 和 map
源码位置:
media/platform/rockchip/cif/cif-rockit.c:149-173
rkcif_buf->mpi_buf =
input_rockit_cfg->mpibuf;
rkcif_init_rockit_vb2(
stream->cifdev,
rkcif_buf);
mem =
g_ops->attach_dmabuf(
&rkcif_buf->vb,
stream->cifdev
->hw_dev->dev,
input_rockit_cfg->buf,
input_rockit_cfg
->buf->size);
if (IS_ERR(mem))
return PTR_ERR(mem);
rkcif_buf->mpi_mem = mem;
rkcif_buf->dmabuf =
input_rockit_cfg->buf;
ret = g_ops->map_dmabuf(mem);
if (ret)
return ret;
if (stream->cifdev
->hw_dev
->is_dma_sg_ops) {
sg_tbl =
g_ops->cookie(
&rkcif_buf->vb,
mem);
rkcif_buf->buff_addr =
sg_dma_address(
sg_tbl->sgl);
} else {
rkcif_buf->buff_addr =
*((u32 *)
g_ops->cookie(
&rkcif_buf->vb,
mem));
}
get_dma_buf(
input_rockit_cfg->buf);
这条路径已经拿到了:
struct dma_buf *
因此不需要先经过 fd。
它直接执行:
attach_dmabuf
↓
map_dmabuf
↓
cookie
↓
sg_dma_address
最终得到 RKCIF 可编程的 DMA 地址。
22.2 对称释放
源码位置:
media/platform/rockchip/cif/cif-rockit.c:269-276
if (rkcif_buf->mpi_mem) {
g_ops->unmap_dmabuf(
rkcif_buf->mpi_mem);
g_ops->detach_dmabuf(
rkcif_buf->mpi_mem);
dma_buf_put(
rkcif_buf->dmabuf);
}
完整生命周期为:
attach
↓
map
↓
取得 DMA address
↓
持有 DMA-BUF 引用
↓
硬件使用
↓
unmap
↓
detach
↓
释放引用
它与标准 V4L2 DMABUF import 的底层原理一致,只是绕过了 V4L2 fd 入口,直接在内核模块之间传递 DMA-BUF 对象。
该路径属于 Rockchip 私有集成,不应当当作通用 V4L2 接口,但非常适合理解共享内存的本质。
二十三、采集到 MPP 编码的完整生命周期
一块缓冲区不能在同一时刻既被采集硬件写入,又被编码硬件读取。
零拷贝能否稳定运行,关键在所有权管理,而不是能否拿到 fd。
推荐状态机如下:
FREE
│
│ QBUF
▼
CAPTURE_QUEUED
│
│ RKCIF 取走
▼
CAPTURING
│
│ 帧完成
│ vb2_buffer_done()
▼
CAPTURE_DONE
│
│ DQBUF
│ 封装 MppFrame
▼
ENCODING
│
│ MPP 不再访问输入帧
▼
FREE
│
└── 再次 QBUF
每个 buffer slot 建议记录:
struct frame_slot {
unsigned int v4l2_index;
int dmabuf_fd[VIDEO_MAX_PLANES];
unsigned int plane_count;
size_t plane_size[VIDEO_MAX_PLANES];
MppBuffer mpp_buf[VIDEO_MAX_PLANES];
enum slot_state state;
uint64_t sequence;
uint64_t timestamp;
};
状态至少应区分:
FREE
CAPTURE_QUEUED
CAPTURING
CAPTURE_DONE
ENCODING
ERROR
二十四、基于 V4L2 导出缓冲的整合骨架
下面代码突出生命周期和句柄传递。
MPP 导入函数名与 buffer type 需要按当前 SDK 头文件校准。
24.1 导出 V4L2 缓冲区
#include <errno.h>
#include <fcntl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/ioctl.h>
static int export_capture_buffer(
int video_fd,
enum v4l2_buf_type type,
unsigned int index,
unsigned int plane,
int *out_fd)
{
struct v4l2_exportbuffer exp;
if (video_fd < 0 ||
out_fd == NULL)
return -EINVAL;
memset(&exp, 0, sizeof(exp));
exp.type = type;
exp.index = index;
exp.plane = plane;
exp.flags = O_CLOEXEC | O_RDWR;
if (ioctl(video_fd,
VIDIOC_EXPBUF,
&exp) < 0)
return -errno;
*out_fd = exp.fd;
return 0;
}
对于 multi-planar 格式,应针对每个独立 memory plane 执行一次 EXPBUF:
for (plane = 0;
plane < plane_count;
++plane) {
ret = export_capture_buffer(
video_fd,
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
index,
plane,
&slots[index]
.dmabuf_fd[plane]);
if (ret)
goto fail;
}
若格式只有一个 memory plane,即使内部包含 Y 和 UV 两个颜色分量,也只需导出一个 fd。
24.2 导入 MPP
不同 MPP 分支可能存在以下两类方式:
mpp_buffer_import()
或者:
external MppBufferGroup
+
mpp_buffer_commit()
下面以常见的 mpp_buffer_import() 形式表达:
static int import_to_mpp(
int dmabuf_fd,
size_t size,
MppBuffer *out)
{
MppBufferInfo info;
MPP_RET ret;
if (dmabuf_fd < 0 ||
size == 0 ||
out == NULL)
return MPP_ERR_VALUE;
memset(&info, 0, sizeof(info));
info.fd = dmabuf_fd;
info.size = size;
/*
* 新分支常见外部 DMA-BUF 类型为
* MPP_BUFFER_TYPE_EXT_DMA。
*
* 旧分支可能使用 DRM、ION,
* 或 external MppBufferGroup。
*
* 实际值必须以对应 SDK 头文件为准。
*/
info.type =
MPP_BUFFER_TYPE_EXT_DMA;
ret = mpp_buffer_import(
out,
&info);
return ret;
}
导入动作不应复制像素。
它只是让:
MppBuffer
引用原 DMA-BUF。
24.3 构造 MppFrame
static MPP_RET build_mpp_frame(
MppFrame *out_frame,
MppBuffer buffer,
int width,
int height,
int hor_stride,
int ver_stride,
MppFrameFormat format)
{
MppFrame frame = NULL;
MPP_RET ret;
if (out_frame == NULL ||
buffer == NULL ||
width <= 0 ||
height <= 0 ||
hor_stride < width ||
ver_stride < height)
return MPP_ERR_VALUE;
ret = mpp_frame_init(&frame);
if (ret != MPP_OK)
return ret;
mpp_frame_set_width(
frame,
width);
mpp_frame_set_height(
frame,
height);
mpp_frame_set_hor_stride(
frame,
hor_stride);
mpp_frame_set_ver_stride(
frame,
ver_stride);
mpp_frame_set_fmt(
frame,
format);
mpp_frame_set_buffer(
frame,
buffer);
*out_frame = frame;
return MPP_OK;
}
这里最容易写错的是:
hor_stride
ver_stride
format
它们必须来自 V4L2 最终生效格式,而不是仅使用请求值。
应以:
VIDIOC_G_FMT
返回的:
bytesperline
sizeimage
width
height
作为依据。
24.4 编码一帧
static MPP_RET encode_one_slot(
MppCtx ctx,
MppApi *mpi,
struct frame_slot *slot,
int width,
int height,
int hor_stride,
int ver_stride)
{
MppFrame frame = NULL;
MPP_RET ret;
if (ctx == NULL ||
mpi == NULL ||
slot == NULL ||
slot->mpp_buf[0] == NULL)
return MPP_ERR_NULL_PTR;
ret = build_mpp_frame(
&frame,
slot->mpp_buf[0],
width,
height,
hor_stride,
ver_stride,
MPP_FMT_YUV420SP);
if (ret != MPP_OK)
return ret;
slot->state = ENCODING;
ret = mpi->encode_put_frame(
ctx,
frame);
mpp_frame_deinit(&frame);
if (ret == MPP_OK)
slot->state = FREE;
else
slot->state = ERROR;
return ret;
}
《MPP 开发参考》描述的简单 encode_put_frame() 模型会等待编码硬件结束对输入帧的使用后再返回,因此该模型下可在返回后重新 QBUF。
若采用异步 task 模式,则必须等待:
输出任务完成
输入对象引用释放
或者 fence 信号
不能在 enqueue() 后立即把同一缓冲重新交给采集硬件。
24.5 主循环
for (;;) {
struct v4l2_buffer vbuf;
struct v4l2_plane planes[VIDEO_MAX_PLANES];
unsigned int index;
int ret;
memset(&vbuf, 0, sizeof(vbuf));
memset(planes, 0, sizeof(planes));
vbuf.type =
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
vbuf.memory =
V4L2_MEMORY_MMAP;
vbuf.m.planes = planes;
vbuf.length = plane_count;
if (ioctl(video_fd,
VIDIOC_DQBUF,
&vbuf) < 0) {
if (errno == EINTR)
continue;
break;
}
index = vbuf.index;
slots[index].state =
CAPTURE_DONE;
slots[index].sequence =
vbuf.sequence;
ret = encode_one_slot(
mpp_ctx,
mpi,
&slots[index],
width,
height,
bytesperline,
aligned_height);
if (ret != MPP_OK)
break;
/*
* 简单阻塞输入模型返回后,
* 同一 slot 才重新交回 RKCIF。
*/
if (ioctl(video_fd,
VIDIOC_QBUF,
&vbuf) < 0)
break;
slots[index].state =
CAPTURE_QUEUED;
}
这段主循环的核心不是 API 数量,而是所有权顺序:
DQBUF
↓
MPP 使用
↓
MPP 完成
↓
QBUF
绝不能写成:
DQBUF
↓
把 fd 交给异步 MPP
↓
立即 QBUF
否则采集 DMA 可能覆盖编码器尚未读取完的数据。
二十五、外部统一缓冲池模式
若希望缓冲池从一开始就由上层统一管理,可以采用:
1. dma-heap、DRM 或 MPP 分配 N 个 DMA-BUF
2. V4L2 REQBUFS(memory = DMABUF)
3. 每次 QBUF 在 plane.fd 中填写对应 fd
4. RKCIF 通过 __prepare_dmabuf() attach/map
5. DQBUF 后把同一 fd 封装为 MppBuffer/MppFrame
6. MPP 完成后重新 QBUF
V4L2 QBUF 示例:
static int queue_dmabuf(
int video_fd,
unsigned int index,
const int *plane_fds,
const size_t *plane_sizes,
unsigned int plane_count)
{
struct v4l2_buffer buf;
struct v4l2_plane planes[VIDEO_MAX_PLANES];
unsigned int i;
if (video_fd < 0 ||
plane_fds == NULL ||
plane_sizes == NULL ||
plane_count == 0 ||
plane_count > VIDEO_MAX_PLANES)
return -EINVAL;
memset(&buf, 0, sizeof(buf));
memset(planes, 0, sizeof(planes));
buf.type =
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory =
V4L2_MEMORY_DMABUF;
buf.index = index;
buf.length = plane_count;
buf.m.planes = planes;
for (i = 0;
i < plane_count;
++i) {
planes[i].m.fd =
plane_fds[i];
planes[i].length =
plane_sizes[i];
}
if (ioctl(video_fd,
VIDIOC_QBUF,
&buf) < 0)
return -errno;
return 0;
}
这条路径省去了 VIDIOC_EXPBUF。
它更适合:
多个硬件模块复用同一缓冲池
统一控制内存总量
统一管理 fence
统一管理 slot 状态
跨进程共享
显示与编码同时使用
但实现时必须保证:
分配器兼容
IOMMU 映射成功
格式兼容
stride 一致
plane 布局一致
缓冲区容量足够
同步关系明确
二十六、解码到显示的共享缓冲
《MPP 开发参考》第 22 至 24 页用流程图区分了三种解码缓冲模式:
内部分配
半内部分配
纯外部分配
内部模式可以做到“解码输出不再复制”,因为输出帧与解码器内部参考帧可引用同一缓冲;但这块内存不一定能被显示系统直接接收。
26.1 内部分配模式
MPP Decoder
↓ 内部分配
MppBuffer
↓
输出 MppFrame
优点:
使用简单
容易快速运行
限制:
缓冲区不一定适合显示控制器
内存总量不容易统一控制
显示零拷贝较难
26.2 纯外部分配模式
Display / DRM / Gralloc
↓ 分配 DMA-BUF
external MppBufferGroup
↓
MPP Decoder 直接写入
↓
Display 直接扫描输出
这时形成:
RKVDEC 写 DMA-BUF
↓
显示控制器读同一 DMA-BUF
它和摄像头到编码器的原理完全对称:
采集到编码:
生产者是 RKCIF/ISP
消费者是 RKVENC
解码到显示:
生产者是 RKVDEC
消费者是显示控制器
区别只在缓冲池由哪一侧建立,以及何时归还给生产者。
二十七、single-planar、multi-planar 与颜色分量
这是零拷贝整合中最容易混淆的概念之一。
27.1 Memory plane
Memory plane 表示独立内存对象。
例如:
2 个 memory plane
↓
2 个 DMA-BUF fd
27.2 Color plane
Color plane 表示图像颜色分量。
例如 NV12:
Y 分量
UV 分量
27.3 两者并不总是一一对应
NV12 可以采用:
mplanes = 1
cplanes = 2
一个 fd:
[Y 数据][UV 数据]
也可以采用:
mplanes = 2
cplanes = 2
fd0:Y
fd1:UV
RKCIF 源码中:
if (fmt->mplanes == 1) {
for (i = 0;
i < fmt->cplanes - 1;
i++) {
cifbuf->buff_addr[i + 1] =
cifbuf->buff_addr[i] +
bytesperline * height;
}
}
正是在处理:
一个 memory plane
多个颜色分量
MPP 侧必须按同一布局解释。
不能看到 NV12 就固定假设:
一定有两个 fd
也不能固定假设:
一定只有一个 fd
应根据 V4L2 的最终 plane 模型决定。
二十八、width 不等于 stride
格式描述至少有四个重要尺寸:
width
可见像素宽度
height
可见像素高度
bytesperline / hor_stride
每行实际占用字节数
aligned height / ver_stride
内存中实际使用的对齐高度
例如:
width = 1920
height = 1080
hor_stride = 2048
ver_stride = 1088
那么单 plane NV12 的内存布局可能为:
Y size = 2048 × 1088
UV size = 2048 × 1088 / 2
总大小为:
2048 × 1088 × 3 / 2
而不是:
1920 × 1080 × 3 / 2
若 MPP 仍按 1920 读取每行,下一行起点就会发生偏移。
因此 MPP 配置应来自:
VIDIOC_G_FMT 返回值
驱动对齐规则
实际 sizeimage
而不是只使用业务配置中的可见宽高。
二十九、像素格式兼容性
DMA-BUF 解决的是:
多个模块共享哪一块内存。
它不解决:
内存里的字节应该怎样解释。
如果采集输出为:
RAW10
而 MPP 编码器输入要求:
NV12
则必须经过:
RKISP
RGA
VPSS
等模块完成转换。
合理路径可以是:
Sensor RAW10
↓
RKCIF/RKISP
↓
NV12 DMA-BUF
↓
MPP Encoder
或者:
V4L2 输出 RGB
↓
RGA 读取 DMA-BUF A
↓
RGA 写入 NV12 DMA-BUF B
↓
MPP Encoder 读取 DMA-BUF B
第二种路径没有 CPU memcpy(),但存在一次硬件转换读写。
因此评估“零拷贝”时,需要说明具体范围:
是否没有 CPU 整帧复制
是否始终复用同一块存储
是否存在硬件格式转换
码流输出是否仍有复制
三十、同步与并发:比 fd 更重要
30.1 不能过早 QBUF
DQBUF 后交给 MPP 的缓冲仍可能被编码器读取。
若此时立即 QBUF:
RKCIF
可能开始覆盖该缓冲
RKVENC
仍在读取旧帧
结果可能是:
撕裂
花屏
随机色块
码流损坏
编码器超时
偶发错误
30.2 需要足够的缓冲数量
缓冲池至少要覆盖:
采集硬件正在写的数量
+
已完成等待编码的数量
+
编码硬件正在读的数量
+
流水线抖动余量
例如:
2 个采集 ping-pong buffer
1 个等待编码 buffer
1 个编码中 buffer
理论下限已经接近 4 个。
若还有:
显示
RGA 转换
网络发送排队
B 帧参考
则需要更多缓冲。
缓冲太少会导致:
采集停顿
编码器等待
QBUF 不足
丢帧
缓冲太多则会导致:
内存占用增加
排队延迟增加
实时性下降
30.3 fence 比固定等待可靠
RKCIF 低延迟路径中可以看到:
dma_fence
sync_file
fence fd
相关代码位于:
media/platform/rockchip/cif/capture.c:6077-6168
它的基本过程为:
dma_fence_init()
↓
sync_file_create()
↓
get_unused_fd_flags()
↓
fd_install()
↓
硬件完成时 dma_fence_signal()
fence 表达的是:
前一个硬件模块已经完成对缓冲区的访问。
它比下面这些方式可靠:
固定 sleep 5 ms
根据帧率猜测完成时间
仅依赖线程调度顺序
在异步零拷贝链路中,fence 通常是安全交接所有权的重要工具。
30.4 引用计数必须完整
共享缓冲可能同时被以下对象引用:
DMA-BUF fd
VB2 plane
dma_buf_attachment
RKCIF 私有 buffer
MppBuffer
显示队列
RGA task
sync_file
关闭 fd 并不一定立即释放内存。
只要其他对象仍持有:
struct dma_buf *
后备页就应继续存在。
反过来,若忘记:
dma_buf_put
dma_buf_detach
dma_buf_unmap_attachment
mpp_buffer_put
关闭导出 fd
释放 MppFrame
就可能形成:
DMA-BUF 泄漏
IOMMU 映射泄漏
CMA 内存耗尽
长期内存增长
streamoff 后无法释放
三十一、如何确认链路真的没有 CPU 大块复制
仅看到 DMA-BUF fd 不足以证明整条链路没有复制。
应从以下层次验证。
31.1 检查队列能力
v4l2-ctl -d /dev/videoX --all
结合源码确认目标节点的:
q->io_modes
包含:
VB2_DMABUF
并确认 ioctl 表具有:
.vidioc_expbuf = vb2_ioctl_expbuf
或者至少支持:
V4L2_MEMORY_DMABUF QBUF
31.2 跟踪关键函数
重点观察:
vb2_ioctl_expbuf
vb2_core_expbuf
vb2_cma_sg_get_dmabuf
__prepare_dmabuf
vb2_cma_sg_attach_dmabuf
vb2_cma_sg_map_dmabuf
rkcif_buf_queue
rkcif_vb_done_oneframe
可以使用:
ftrace
function_graph
trace_printk
动态调试
受控日志
记录调用顺序。
31.3 搜索可疑 CPU 搬运
在整合代码和驱动中检查:
memcpy
memmove
memset
memcpy_toio
copy_to_user
copy_from_user
mmap 后的整帧循环
软件颜色转换
软件缩放
copy_to_user() 传递小型 IOCTL 元数据并不构成图像复制。
关键是是否按照:
sizeimage
width × height
整帧 plane size
量级搬运像素。
31.4 观察 CPU 与 DDR 负载
在分辨率和帧率不变的情况下,对比:
整帧 memcpy 版本
DMA-BUF 直通版本
观察:
CPU 占用
内存带宽
Cache miss
帧延迟
丢帧计数
编码吞吐
温度和功耗
若 CPU 占用明显下降、内存带宽减小,同时输出内容一致,才说明优化真正落到了数据面。
31.5 打印句柄与地址关系
调试时可以记录:
V4L2 buffer index
DMA-BUF fd
struct dma_buf 指针
sg_table 指针
设备 DMA address
MppBuffer 句柄
frame sequence
timestamp
同一个 slot 在不同模块中应保持稳定对应关系。
需要注意:
DMA address 在不同设备或不同 IOMMU 域中可能不同,因此不能仅凭地址不同就判断发生了复制。
三十二、常见误区
误区一:使用 mmap() 就是零拷贝
MMAP 只表示进程把 VB2 缓冲映射到地址空间。
若随后执行:
memcpy(mpp_buffer,
v4l2_buffer,
frame_size);
仍然存在一次整帧复制。
误区二:传 fd 就必然没有复制
fd 可能被某个中间层导入后,又执行:
格式转换
软件叠加
重新分配
复制到内部缓冲
必须沿实际代码确认下游是否直接:
attach
map
读取原 DMA-BUF
误区三:IOMMU 会复制图像
IOMMU 主要负责:
地址转换
访问权限
设备隔离
把 SG 页映射到设备地址空间
建立 IOVA 映射不等于复制页内容。
误区四:零拷贝就不需要同步
不复制反而使所有权和同步更重要。
必须满足:
生产者写完之前,消费者不能读
消费者读完之前,生产者不能覆盖
误区五:格式名称相同,布局就一定相同
同为 NV12,仍可能存在:
不同 stride
不同对齐高度
单 memory plane
双 memory plane
压缩布局
额外 header
不同色彩范围
必须比较完整格式描述,而不是只比较 fourcc。
误区六:编码器输入零拷贝等于全链路零拷贝
原始图像可以直接以 MppBuffer 输入,但简单 encode_get_packet() 仍可能复制码流。
是否需要继续优化,应根据:
码流大小
吞吐目标
接口复杂度
延迟目标
进行权衡。
误区七:缓冲区越少,延迟一定越低
缓冲区太少会导致:
采集等待编码
硬件流水线无法并行
QBUF 数不足
抖动增大
低延迟需要的是:
足够维持流水线
但不过度排队
而不是盲目把缓冲数量降到最小。
三十三、架构设计建议
一条稳定的 Rockchip 零拷贝视频链路应遵循以下原则。
1. 先统一格式,再统一内存
明确:
fourcc
memory plane 数
color plane 数
width
height
bytesperline
aligned height
sizeimage
色彩范围
2. 建立固定缓冲池
每个 slot 记录:
V4L2 index
DMA-BUF fd
MppBuffer
plane size
当前状态
sequence
timestamp
fence
3. 只在边界传递句柄
模块之间传递:
fd
struct dma_buf *
buffer index
时间戳
格式元数据
不传递整帧副本。
4. 保持 attach/map 对称
每一次:
attach
map
都必须存在对应:
unmap
detach
5. 以完成事件交接所有权
V4L2 使用:
vb2_buffer_done
DQBUF
MPP 使用:
输入完成
task 完成
引用释放
fence
6. 减少 CPU 访问像素
确实需要图像处理时,优先考虑:
RKISP
ISPP
VPSS
RGA
并继续使用 DMA-BUF 作为输入输出。
7. 区分公共接口与私有快路径
标准 V4L2 DMABUF:
兼容性好
调试工具完善
应用移植更容易
Rockit 内核直连:
封装层次更少
平台耦合更强
依赖私有接口
8. 按实际 SDK 校准 MPP 导入 API
所附 MPP 文档版本较早。
不同分支中可能出现:
MppBufferGroup
mpp_buffer_commit
mpp_buffer_import
MPP_BUFFER_TYPE_DRM
MPP_BUFFER_TYPE_ION
MPP_BUFFER_TYPE_DMA_HEAP
MPP_BUFFER_TYPE_EXT_DMA
机制相同,但接口名称和 buffer type 需要以当前 SDK 头文件为准。
结语
Rockchip 平台上的零拷贝不是某一个 API,也不是简单地把 memcpy() 删除。
它是一套贯穿媒体栈的共享缓冲架构:
V4L2/VB2
负责缓冲区状态机
DMA-BUF
负责跨模块共享后备存储
IOMMU/SG
负责建立设备可访问的 DMA 映射
RKCIF/RKISP/RGA/VPU
负责直接读写共享内存
MPP
使用 MppBuffer/MppFrame
描述并调度硬件任务
fence、引用计数和队列状态
负责所有权交接
从源码可以看到完整证据链:
RKCIF 队列开放 VB2_DMABUF
↓
VIDIOC_EXPBUF 可导出 MMAP 缓冲
↓
VB2 Core 调用 get_dmabuf
↓
dma_buf_export 创建共享对象
↓
外部 fd 可重新导入
↓
__prepare_dmabuf 执行 attach/map
↓
rkcif_buf_queue 取得 DMA address
↓
硬件直接写入
↓
vb2_buffer_done 只改变状态
↓
同一个 DMA-BUF 可交给 MPP
CIF 到 ISP 的内部路径以及 Rockit 私有路径也直接传递同一个 DMA-BUF 对象,而不是复制整帧图像。
真正高质量的零拷贝实现,最终要同时满足四个条件:
内存可共享
格式可兼容
所有权可追踪
同步可证明
只满足“拿到了一个 fd”还不够。
只有同一块后备存储能够在采集、处理、编码或显示之间安全轮转,并且没有隐藏的整帧 CPU 搬运,才构成完整的 Rockchip 媒体零拷贝链路。