一个跑在树莓派 5 + Ubuntu24 + IMX219 上的小项目,用来把 GStreamer 端到端的 dma-buf 零拷贝通路彻底跑对。上游从 CSI 或 UVC 拿到帧,一路 dma-buf 送到 v4l2h264enc 硬编,落盘 + 可选 RTSP 预览;也可以走另一条支线:直接把 dma-buf 交给 kmssink,让 DRM primary plane 做 scanout,在 Pi 5 没有 H.264 硬编时得到一条真正 CPU 零参与的显示链路。在直显形态下还提供两个 KMS overlay plane 的合成层:右上角时间戳水印(CPU 光栅化 8x16 点阵,一秒一次),以及 libpag 动画 overlay(GPU 直接画进 dma-buf backed FBO,再以 DRM fb 绑到 overlay plane)。整条链路不下地、不 memcpy,运行时能看到编码器 / 显示器入口 caps 上确实带着 memory:DMABuf,overlay plane 在 vc4 HVS 里与主视频同帧硬件合成。
在真板子上把 CSI camera → dma-buf → HW encoder → RTSP 这条链路彻底跑对,并且可复现地验证零拷贝没被打断。 编码分支不可用(例如 Pi 5 上没有 H.264 硬编)时,提供 kmssink 直显作为对照组,以理解"零拷贝"和"是否经过编码"是两件事。直显形态下再叠一层业务需求常见的 overlay(时间戳水印 / PAG 动画),验证 "GPU/CPU 生成内容 → dma-buf → KMS overlay plane → HVS 硬件合成" 这条 c-side 通路的可行性,同时把 EGL/GBM、DRM master fd 借用、DRM plane 竞争等边角都真跑一遍。
硬件:Raspberry Pi 5 (8G) + Sony IMX219(CSI)或任意 UVC。系统:Ubuntu 24.04。
sudo apt install -y \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
gstreamer1.0-tools gstreamer1.0-plugins-{base,good,bad,ugly} \
gstreamer1.0-libav gstreamer1.0-libcamera \
libgstrtspserver-1.0-dev gstreamer1.0-rtsp \
libdrm-dev libgbm-dev libegl-dev libgles-dev \
libspdlog-dev libyaml-cpp-dev cmake ninja-build g++
cmake -S . -B cmake-build-raspi -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DTARGET_PLATFORM=rpi
cmake --build cmake-build-raspi -j
# 默认在当前目录自动搜索 config/default.yaml
./cmake-build-raspi/vm_camera_dma_buf
# 也可以显式指定配置,或用 CLI 覆盖单个字段
./cmake-build-raspi/vm_camera_dma_buf --config config/default.yaml
./cmake-build-raspi/vm_camera_dma_buf --device /dev/video2libpag(PAG overlay 支线)由第三方源码在 third_party/libpag 下随项目一起构建,默认由 CMake 开关 ENABLE_PAG_OVERLAY=ON 打开。若不需要 PAG overlay,可以 -DENABLE_PAG_OVERLAY=OFF 关掉,此时 src/overlay/pag/ 下三个 .cpp 不参与编译、也不链接 libpag。
Ubuntu 24.04 for Raspberry Pi 官方源里的 libcamera / gstreamer1.0-libcamera 版本与 Pi 5 的 IMX219 CSI 通路对不上,apt 装完之后 rpicam-hello --list-cameras 直接空列表,libcamerasrc 也起不来。所以真板子上这一步是绕不开的:需要从源码手动编译 libcamera(以及可选的 rpicam-apps 生态)。
具体流程直接参考这篇踩坑记录:
vm_camera_dma_buf/
├── CMakeLists.txt # 构建入口
├── config/ # 运行时 YAML 配置 (config/default.yaml)
├── assets/ # 运行期资源 (PAG 素材等)
│ └── pag/PAG_LOGO.pag
├── docs/ # 分模块的详细设计文档
│ ├── dma_buf_lifecycle.md
│ ├── drm_api_reference.md
│ ├── hdmi_display_pipeline.md
│ ├── timestamp_overlay_pipeline.md
│ └── pag_overlay_pipeline.md
├── scripts/ # 环境自检 / 辅助脚本
├── src/ # C++ 源码主目录
│ ├── main.cpp / app.{h,cpp} # 程序入口 + 全局装配 (build_and_start)
│ ├── util/ # 配置、日志、dma-buf 探测、caps 工具等
│ ├── buffer/ # dma-buf allocator / pool 预留位
│ ├── pipeline/ # GStreamer pipeline 组装
│ │ ├── rpi_pipeline.cpp # CSI / UVC dma-buf / UVC mmap 三分支
│ │ ├── encoder_branch.{h,cpp} # tee + v4l2h264enc + filesink (+ 可选 rtsp-tap)
│ │ ├── display_branch.{h,cpp} # queue + kmssink 直显 (与 encoder 互斥)
│ │ ├── pipeline_builder.h
│ │ └── pipeline_factory.cpp
│ ├── sink/ # appsink 回调、抓帧、RTSP server 封装
│ └── overlay/ # 显示形态下叠加在 kmssink 之上的 DRM overlay
│ ├── timestamp_overlay.{h,cpp} # 8x16 点阵水印 → dumb-buffer plane
│ ├── drm/ # DRM plane 通用件 (session/picker/dumb_fb)
│ └── pag/ # libpag 动画 overlay 支线
│ ├── egl_gbm_context.{h,cpp} # /dev/dri/renderD128 + EGL/GBM
│ ├── dmabuf_fb_ring.{h,cpp} # dma-buf ↔ GL FBO ↔ DRM fb 三位一体 ring
│ └── pag_overlay.{h,cpp} # libpag → FBO 渲染线程 + KMS 显示线程
├── third_party/ # 第三方组件源码 (libpag、mediamtx 等)
└── web/ # 浏览器预览壳(配合 RTSP 分支使用)
三种形态互斥/叠加规则:encoder.enable 与 display.enable 互斥(编码链路里最后一级是 filesink,直显链路里最后一级是 kmssink,共享同一个源 tee 的下游需要用配置在两者中二选一);display.enable=true 之后,display.timestamp.enable / display.pag_overlay.enable 可以各自独立打开,两者与主视频在 KMS 侧属于不同 plane,由 vc4 HVS 硬件合成。
最完整的形态,实际生成的图接近:
libcamerasrc
! video/x-raw(memory:DMABuf), NV12, 1920x1080@30
! tee name=t
t. ! queue leaky=downstream max-size-buffers=2
! appsink # 主干,业务侧
t. ! queue leaky=downstream max-size-buffers=4
! v4l2h264enc extra-controls="..."
! h264parse config-interval=1
! tee name=t2
t2. ! queue ! filesink location=/tmp/gst_dump/out.h264
t2. ! queue ! appsink name=rtsp-tap # 供 RTSP 拉
rtsp-tap 侧的 sample 会被 pump 线程读出来,跨 pipeline 送到 RTSP factory 内部的 appsrc,由 rtph264pay 打包发出:
appsrc (do-timestamp=true, format=time)
! video/x-h264, byte-stream, au
! queue leaky=downstream max-size-buffers=8
! rtph264pay config-interval=1
必须 encoder.enable=false display.enable=true,走 kmssink 直显。这是 Pi 5 上"没有 H.264 硬编、但仍要看画面"的落地形态:
libcamerasrc
! video/x-raw(memory:DMABuf), NV12, 1920x1080@30
! queue leaky=downstream max-size-buffers=2
! kmssink force-modesetting=true can-scale=true sync=false
display.timestamp.enable / display.pag_overlay.enable 打开后,我们会 借用 kmssink 的 DRM master fd(dup(kmssink.fd))另开独立生命线,各自 drmModeSetPlane() 挂到一个 overlay plane 上。GStreamer 侧的 pipeline 不改,vc4 HVS 在 scanout 时把三层合到一起:
[主视频] libcamerasrc → dma-buf → kmssink → DRM primary plane ─┐
│
[时间戳] CPU 8x16 点阵 (每秒一次) → dumb-buffer → overlay plane ─┤─→ HVS 硬件合成 → HDMI
│
[PAG 动画] libpag GLES2 → dma-buf FBO (30/60fps) → overlay plane ─┘
↑
(GPU 直接写 dma-buf,无 CPU 拷贝)