深度解析 RK3518 平台 OpenGL 绿幕扣图 + DRM Buffer HDMI 送显:MEDAI-W2 拉流解码器的 GPU 合成与零拷贝显示全链路
产品背景:MEDAI-W2 是一款基于瑞芯微 RK3518 平台的拉流解码器,可通过网络拉取 RTSP/RTMP 等流媒体信号,解码后输出到 HDMI,并支持 UVC(USB Video Class)与 NET1 两路视频通道。设备内置 Web 管理系统(固件版本 2.1),在"解码设置"首页实时展示 CPU / 内存 / 核心温度,并并列呈现 HDMI1、UVC、NET1 三个输出通道状态。
前文已分别解析了 UVC 输入 × MPP 硬解码 与 V4L2/UVC 框架与自动格式协商 两条链路。本文继续深挖解码后的处理环节:如何利用 RK3518 的 Mali-450 GPU(OpenGL ES)做绿幕扣图合成 ,以及合成结果如何经由 DRM Buffer 零拷贝送显到 HDMI 。这套"GPU 合成 + DRM 直显"路径,正是 Web 首页 CPU 长期低位(说明书示例 7%)却仍能稳定输出 1080p/4K 画面的关键------扣图与合成不占 CPU,显示不走 CPU 拷贝。

一、RK3518 显示与 GPU 架构总览
1.1 为什么需要 GPU 与 DRM 这条链路
MEDAI-W2 的完整数据通路(见《使用说明书》3.3 节"输出状态")是:
网络拉流 / UVC 输入 → MPP VPU 硬解 → NV12 帧 → (本文链路)→ HDMI1 输出
解码只是第一步。当业务要求把一路带绿幕的前景画面与另一路背景画面实时合成(虚拟演播、直播抠像、会议背景替换)时,若用 CPU 做逐像素抠像 + 混合,1080p30 就要处理近 2 亿次/秒像素运算,CPU 必然被打满。RK3518 给出的答案是:
- GPU(Mali-450 MP2,OpenGL ES 2.0) 负责绿幕扣图与多纹理合成------并行 fragment shader 一次算完整帧;
- DRM/KMS 负责把 GPU 渲染好的帧缓冲直接扫描输出到 HDMI,无需任何 CPU 回读/拷贝。
1.2 软件栈分层
┌──────────────────────────────────────────────────┐
│ 应用层:MEDAI-W2 视频服务(拉流 + 抠像合成 + 显示) │
├──────────────────────────────────────────────────┤
│ 用户空间框架:MPP / RGA / EGL / GBM / DRM-KMS │
│ ├─ OpenGL ES 2.0(Mali-450 驱动:limare/armsoc) │
│ ├─ EGL(KHR_platform_gbm) │
│ ├─ GBM(Generic Buffer Manager) │
│ └─ libdrm(drmModePageFlip 等) │
├──────────────────────────────────────────────────┤
│ 内核层:drm / rockchip-drm / mali 驱动 │
│ ├─ DRM 主设备 / 渲染节点(/dev/dri/card0) │
│ ├─ VOP(Video Output Processor,显示控制器) │
│ └─ HDMI 控制器 │
├──────────────────────────────────────────────────┤
│ 硬件:Mali-450 GPU / RGA / VOP / HDMI PHY │
└──────────────────────────────────────────────────┘
核心思想:RGA 做色彩空间转换、GPU 做像素合成、VOP 通过 DRM 做扫描输出,三者通过 GBM/dmabuf 缓冲共享,全程零拷贝。
二、绿幕扣图(Chroma Key)技术原理
2.1 什么是绿幕扣图
绿幕扣图(Chroma Key / Green Screen Keying)是指:在前景画面上,把近似于"标准绿"(或蓝)的像素识别为"透明",再与一个背景图层做 alpha 混合,从而得到"人/物体在任意背景前"的合成画面。
判定一个像素是否该被抠掉,本质是计算该像素颜色与"键色(Key Color)"的相似度:
- 相似度高 → 判定为绿幕 → alpha = 0(透明,露出背景);
- 相似度低 → 判定为前景 → alpha = 1(保留);
- 中间过渡区 → 做平滑(smoothstep),避免边缘锯齿。
2.2 两种常用判定空间
| 判定空间 | 做法 | 优缺点 |
|---|---|---|
| RGB 欧氏距离 | 直接在 RGB 空间 distance(fg, keyColor) |
实现最简单;对光照/阴影敏感 |
| YCbCr / YUV 色度距离 | 只比较 Cb、Cr(色度),忽略亮度 Y | 对亮度变化鲁棒,抠像更干净,是工程首选 |
在 Mali-450(仅 OpenGL ES 2.0,无 OpenCL/Compute)上,无论哪种空间,都在 fragment shader 里逐像素并行完成------这正是 GPU 擅长的"单指令多数据"场景。
2.3 解码帧到纹理的色彩路径
MPP VPU 硬解原生输出为 NV12(YUV420SP)(《MPP 硬解码》一文 2.3 节已说明)。GPU 纹理通常吃 RGB/RGBA。两种接入方式:
- RGA 预转换:用 RK3518 的 RGA(2D 加速单元)把 NV12 → RGBA,结果写入 GBM buffer,再作为 GL 纹理;
- Shader 内联转换:直接把 NV12 的两张平面(Y plane + 交错的 UV plane)作为两个纹理上传,在 fragment shader 内做 YUV→RGB。
工程上推荐 方案 1(RGA 转 RGBA):转换交给 RGA,GPU 专心做抠像合成,职责清晰且可利用 dmabuf 在 RGA 与 GPU 间零拷贝传递。
三、OpenGL ES 绿幕扣图实现
3.1 渲染拓扑
前景纹理(带绿幕 RGBA) ┐
├─→ fragment shader(抠像+混合) ─→ 输出到 GBM 帧缓冲
背景纹理(替换背景 RGBA) ┘
每帧流程:
- 绑定前景纹理、背景纹理;
- 传入键色(
uKeyColor)、阈值(uThreshold)、平滑宽度(uSmoothEdge); - 画一个铺满视口的四边形(两个三角形);
- fragment shader 对每个片元计算色度距离 → 算 alpha → 与背景混合 → 写 RGBA 到帧缓冲。
3.2 绿幕扣图 Fragment Shader(GLSL ES 2.0)
Mali-450 仅支持 OpenGL ES 2.0 / GLSL ES 1.00 ,因此使用
precision mediump float与texture2D。
glsl
/* 绿幕扣图 fragment shader:YCbCr 色度距离 + smoothstep 边缘平滑 */
precision mediump float;
uniform sampler2D uForeground; /* 前景:带绿幕的画面 (RGBA) */
uniform sampler2D uBackground; /* 背景:替换背景 (RGBA) */
uniform vec3 uKeyColor; /* 键色(绿幕标准绿,RGB 0~1) */
uniform float uThreshold; /* 抠像阈值:色度距离小于它即透明 */
uniform float uSmoothEdge; /* 边缘平滑宽度,避免锯齿 */
varying vec2 vTexCoord;
/* RGB → YCbCr,仅比较色度 Cb/Cr,对亮度变化更鲁棒 */
vec2 rgb2cbcr(vec3 c) {
float Y = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
float Cb = -0.169 * c.r - 0.331 * c.g + 0.500 * c.b + 0.5;
float Cr = 0.500 * c.r - 0.419 * c.g - 0.081 * c.b + 0.5;
return vec2(Cb, Cr);
}
void main() {
vec4 fg = texture2D(uForeground, vTexCoord);
vec4 bg = texture2D(uBackground, vTexCoord);
/* 前景与键色的色度距离 */
vec2 d = rgb2cbcr(fg.rgb) - rgb2cbcr(uKeyColor);
float dist = length(d);
/* alpha:距离小 → 0(透明/抠掉),距离大 → 1(保留前景) */
float alpha = smoothstep(uThreshold - uSmoothEdge,
uThreshold + uSmoothEdge,
dist);
/* 合成:透明处露出背景,非透明处保留前景 */
gl_FragColor = mix(bg, fg, alpha);
}
要点:
- 色度距离优于 RGB 距离:人的头发边缘常有绿幕反光(溢出),用 Cb/Cr 距离比 RGB 距离更能干净地抠掉绿边;
smoothstep做软抠像 :硬阈值会留下锯齿,边缘平滑宽度uSmoothEdge通常取阈值的 10%~20%,可让发丝边缘更自然;- 可调参数 :直播场景可把
uThreshold做成 Web 端滑块(对应说明书"音频配置"式的可调 UI),实时微调抠像强度。
3.3 顶点 Shader 与绘制闭环
glsl
/* 顶点 shader:铺满视口的四边形 */
attribute vec2 aPos;
attribute vec2 aUV;
varying vec2 vTexCoord;
void main() {
vTexCoord = aUV;
gl_Position = vec4(aPos, 0.0, 1.0);
}
c
/* 每帧绘制:绑定两纹理 + 设置 uniforms + draw */
glUseProgram(prog);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, fg_tex); /* 前景(RGA 转好的 RGBA) */
glUniform1i(uForeground, 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, bg_tex); /* 背景 */
glUniform1i(uBackground, 1);
glUniform3f(uKeyColor, 0.0f, 1.0f, 0.0f); /* 标准绿 */
glUniform1f(uThreshold, 0.18f);
glUniform1f(uSmoothEdge, 0.03f);
glBindBuffer(GL_ARRAY_BUFFER, quad_vbo);
glEnableVertexAttribArray(aPos);
glVertexAttribPointer(aPos, 2, GL_FLOAT, GL_FALSE, 16, 0);
glEnableVertexAttribArray(aUV);
glVertexAttribPointer(aUV, 2, GL_FLOAT, GL_FALSE, 16, 8);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
四、DRM Buffer HDMI 送显原理
4.1 DRM/KMS 是什么
DRM(Direct Rendering Manager)是 Linux 内核的显示子系统,KMS(Kernel Mode Setting)是其模式设置部分。它把显示硬件抽象为四个对象:
| 对象 | 作用 | 在 MEDAI-W2 中 |
|---|---|---|
| Connector | 物理接口(HDMI/DP) | HDMI1 接口 |
| Encoder | 把 CRTC 时序编码成接口信号 | HDMI 编码器 |
| CRTC | 扫描控制器(扫描缓冲 → 屏幕) | VOP 显示控制器 |
| Plane / Framebuffer | 待扫描的像素缓冲 | GBM 分配的 RGBA 缓冲 |
"送显"的本质:把一个 Framebuffer(一段显存/内存缓冲)绑定到 CRTC,让 VOP 按固定时序把它扫到 HDMI 上。
4.2 GBM:GPU 与 DRM 之间的"共享内存"
要让 GPU 渲染的结果直接被 DRM 扫描,二者必须共用同一块缓冲------这就是 GBM(Generic Buffer Manager) 的角色:
gbm_surface_create()分配一组可同时用于"GPU 渲染目标"和"DRM 扫描输出"的缓冲(GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);- EGL 在 GBM surface 上创建渲染上下文,OpenGL 直接渲染进这些缓冲;
- 渲染完成后
gbm_surface_lock_front_buffer()取出前置缓冲,把它注册为 DRM Framebuffer,再drmModePageFlip翻到屏幕。
这一步实现了 GPU 渲染 → DRM 扫描 的零拷贝 :没有 glReadPixels 回读,没有 memcpy 到用户态。
4.3 EGL + GBM + DRM 显示初始化(参考实现)
c
#include <gbm.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
/* 1. 打开 DRM 设备 */
int drm_fd = open("/dev/dri/card0", O_RDWR);
/* 2. 获取 CRTC / Connector / Mode(省略资源遍历,取第一个已连接 HDMI) */
drmModeRes *res = drmModeGetResources(drm_fd);
drmModeConnector *conn = ...; /* 找到 status==connected 的 HDMI */
drmModeModeInfo mode = conn->modes[0];
uint32_t conn_id = conn->connector_id;
uint32_t crtc_id = ...; /* 与 encoder 绑定的 CRTC */
/* 3. 创建 GBM 设备与 surface */
struct gbm_device *gbm = gbm_create_device(drm_fd);
struct gbm_surface *gs = gbm_surface_create(
gbm, mode.hdisplay, mode.vdisplay,
GBM_FORMAT_ARGB8888,
GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
/* 4. EGL 绑定 GBM 平台 */
EGLDisplay egl_dpy = eglGetPlatformDisplay(EGL_PLATFORM_GBM_MESA, gbm, NULL);
eglInitialize(egl_dpy, NULL, NULL);
EGLConfig cfg = choose_config(egl_dpy, EGL_WINDOW_BIT);
EGLSurface egl_surf = eglCreateWindowSurface(egl_dpy, cfg, gs, NULL);
EGLContext egl_ctx = eglCreateContext(egl_dpy, cfg, EGL_NO_CONTEXT, NULL);
eglMakeCurrent(egl_dpy, egl_surf, egl_surf, egl_ctx);
/* 5. 初始化 OpenGL 抠像程序(见第三节),然后进入渲染循环 */
while (running) {
render_chroma_key_frame(); /* OpenGL 渲染到 GBM 前置缓冲 */
/* 6. 取前置缓冲并注册为 DRM Framebuffer */
struct gbm_bo *bo = gbm_surface_lock_front_buffer(gs);
uint32_t fb_id = 0;
uint32_t stride = gbm_bo_get_stride(bo);
uint32_t handle = gbm_bo_get_handle(bo).u32;
drmModeAddFB(drm_fd, mode.hdisplay, mode.vdisplay,
24, 32, stride, handle, &fb_id);
/* 7. 翻页送显(page flip,双缓冲) */
drmModePageFlip(drm_fd, crtc_id, fb_id, DRM_MODE_PAGE_FLIP_EVENT, NULL);
eglSwapBuffers(egl_dpy, egl_surf); /* 交换 GBM 前后缓冲 */
gbm_surface_release_buffer(gs, bo);
}
4.4 双缓冲与翻页事件
- 双缓冲 :GBM surface 维护前/后两个缓冲,GPU 在后缓冲渲染,DRM 扫描前缓冲,
eglSwapBuffers交换------避免"渲染一半就被扫到屏上"的撕裂; DRM_MODE_PAGE_FLIP_EVENT:翻页是异步的,内核在 VBlank 完成时回调,应用据此调度下一帧,天然实现 vsync 同步,抖动最小。
五、MEDAI-W2 全链路:零拷贝绿幕合成 + HDMI 直显
5.1 管线全景
┌──────────────────────────────────────┐
│ MEDAI-W2 (RK3518) │
│ │
RTSP/RTMP 拉流 ──┐ │ ┌─────────┐ ┌────────────────┐ │
或 UVC 输入 ─┼─→ │ MPP VPU │ → │ NV12 帧(dmabuf)│ │
│ │ │ 硬解 │ └───────┬────────┘ │
│ │ └─────────┘ │ │
│ │ ▼ │
│ │ ┌──────────────┐ │
│ │ │ RGA 色彩转换 │ │ NV12 → RGBA
│ │ │ (dmabuf 零拷贝)│ │
│ │ └──────┬───────┘ │
│ │ ▼ │
│ │ 背景纹理 ──┐ ┌────────────────┐ │
│ │ ├─→ │ OpenGL ES 渲染 │ │ Mali-450
│ │ 前景纹理 ──┘ │ 绿幕扣图合成 │ │ fragment shader
│ │ └─→ └──────┬───────┘ │
│ │ ▼ │
│ │ ┌────────────────┐ │
│ │ │ GBM/dmabuf 缓冲│ │ GPU 渲染目标
│ │ └──────┬───────┘ │ = DRM 扫描缓冲
│ │ ▼ │
│ │ ┌────────────────┐ │
│ │ │ DRM/KMS 翻页 │ │ page flip
│ │ │ (零拷贝送显) │ │
│ │ └──────┬───────┘ │
└─┼───────────────────────┼──────────┘
│ ▼
│ HDMI 显示器
│
(同一合成帧也可编码 → Web 双路预览 / NET1 输出)
5.2 dmabuf 如何贯穿全链路
这是整条链路"零拷贝"的核心:
- VPU 输出 NV12 即是一个 dmabuf fd(
MppBuffer,基于 DMA-BUF/ION,见《MPP 硬解码》2.4 节); - RGA 导入该 dmabuf ,把 NV12 转 RGBA,写出到 GBM 分配的 dmabuf(RGA 支持 dmabuf 输入输出);
- EGL 用
EGL_EXT_image_dma_buf_import把 GBM/dmabuf 包装成EGLImageKHR,再glEGLImageTargetTexture2DOES绑定为 GL 纹理------无需把像素搬回 CPU; - Shader 合成结果直接写回另一块 GBM/dmabuf;
- 该 GBM buffer 被 DRM
drmModeAddFB直接注册为扫描帧缓冲 ,drmModePageFlip送出。
全程 无 glReadPixels、无 memcpy,CPU 仅做调度与参数下发------这正是说明书首页 CPU 使用率长期低位(7%)却仍能稳定抠像 + 合成 + 显示的根本原因。
5.3 与 Web 管理系统的映射
- HDMI1 通道状态:直接由上述 DRM/KMS 链路是否成功翻页驱动,对应说明书 3.3 节"输出状态"区的 HDMI1 卡片;
- 画面预览(UVC / NET1):合成完成的帧可再经 VPU/H.264 编码推给 Web 端做双路预览(与《UVC 输入 × MPP》3.1 节的四路消费一致);
- 系统监控:CPU 低、温度可控,正是 GPU + DRM 卸载成功的体现。
六、调试与验证
6.1 显示与 DRM 侧
bash
# 确认 DRM 设备节点
ls -l /dev/dri/
# 列出显示管线(connector / encoder / crtc / mode)
modetest -c # 查看 connector 与当前 mode
modetest -p # 查看 plane 支持格式
# 内核 DRM 驱动日志
dmesg | grep -i drm
dmesg | grep -i rockchip
# 实时观察翻页/vsync 中断
cat /proc/interrupts | grep -i hdmi
6.2 GPU / EGL 侧
bash
# 确认 Mali 驱动与 EGL 可用
ls /dev/mali*
eglinfo # 列出 EGL 配置(需安装 mesa-utils)
# 确认 GBM/EGL 扩展支持(关键零拷贝路径依赖)
eglinfo | grep -i "EGL_EXT_image_dma_buf_import"
eglinfo | grep -i "EGL_PLATFORM_GBM_MESA"
# 合成后取一帧验证(调试期用,量产应走零拷贝直显)
# 可用 gstreamer 的 kmssink / cluttersink 做对照
6.3 典型问题定位
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| HDMI 黑屏 | GBM 格式不被 VOP 支持(如用 RGB565) | modetest -p 确认 plane 支持 ARGB8888/XRGB8888 |
| 抠像边缘绿边 | 阈值过低或未用色度距离 | 调大 uThreshold、改用 Cb/Cr 距离(3.2 节) |
| 画面撕裂 | 未做双缓冲 / 翻页未等 VBlank | 检查 4.3/4.4 节双缓冲与 page flip 事件 |
| CPU 异常飙高 | 走了 glReadPixels 回读再拷贝 |
确认走 EGLImage + dmabuf,无回读路径 |
| 帧率上不去 | RGA 转 RGBA 成瓶颈 | 确认 RGA 走 dmabuf 直通,未落盘/落内存 |
| 切换输入源花屏 | 旧帧未 flush(同前文 UVC 切换) | 复用《V4L2/UVC》5.4 节管线 flush 逻辑 |
七、总结
MEDAI-W2 基于 RK3518 的"绿幕扣图 + HDMI 送显"链路,可以归纳为 "一核调度、两级加速、一条总线、零次拷贝":
- 一核调度:CPU 只做协议栈、参数下发与翻页事件调度,不碰像素;
- 两级加速:RGA 负责 NV12→RGBA 色彩转换,Mali-450 GPU 负责绿幕抠像与多纹理合成;
- 一条总线:GBM/dmabuf 贯穿 VPU → RGA → GPU → DRM,缓冲在设备间以 fd 形式流转;
- 零次拷贝 :
EGLImage导入 dmabuf 做纹理、GPU 渲染结果直接被drmModePageFlip扫描,全程无memcpy/无glReadPixels。
把"抠像 + 合成 + 显示"三件事从 CPU 上彻底卸下,既保住了说明书首页那抹低 CPU(7%)与可控温度,又让 MEDAI-W2 在 1080p/4K 下稳定输出虚拟背景画面。这套 EGL+GBM+DRM 的零拷贝显示范式,同样适用于 RK35 系列(RK3518/RK3528/RK3568/RK3576)上的视频合成、虚拟演播、直播抠像与商业显示产品,可直接作为嵌入式图形工程的参考实现。
参考资料
- 《MEDAI-W2 拉流解码器使用说明书》(固件版本 2.1,文档版本 V1.0)
- 《MEDAI-W2 UVC 输入 × MPP 硬解码》技术解析(同系列文章)
- 《MEDAI-W2 V4L2/UVC 框架与自动格式协商》技术解析(同系列文章)
- Linux Kernel Documentation:
Documentation/gpu/drm-kms.rst、Documentation/gpu/drm-mm.rst- Mesa / libdrm:
gbm.h、xf86drmMode.h与 EGL_EXT_image_dma_buf_import 扩展说明- 瑞芯微 RK3518 平台技术资料(Mali-450、RGA、VOP/HDMI 章节)