找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4403

积分

0

好友

571

主题
发表于 2 小时前 | 查看: 2| 回复: 0

前言

这篇文章主要讲解 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 页明确区分 MppMemMppBuffer

MppMem
    普通 C 内存封装

MppBuffer
    面向硬件可访问的 DMA-BUF 内存

MppFrameMppPacket 可以引用 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 页对 widthheighthor_stridever_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/dequeueMppTask 任务接口。

因此:

“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 的 preparefinish

源码位置:

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 媒体零拷贝链路。




上一篇:Claude Code 多 Agent 软件工厂:如何让功能在你睡觉时自动交付
下一篇:Citrix NetScaler CVE-2026-8452 预认证RCE漏洞分析:SAML规范化堆溢出利用链
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-17 04:59 , Processed in 0.988015 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表