MEDAI-W2_OpenGL绿幕扣图_DRMBUF_HDMI送显_CSDN文章

深度解析 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。两种接入方式:

  1. RGA 预转换:用 RK3518 的 RGA(2D 加速单元)把 NV12 → RGBA,结果写入 GBM buffer,再作为 GL 纹理;
  2. 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) ┘

每帧流程:

  1. 绑定前景纹理、背景纹理;
  2. 传入键色(uKeyColor)、阈值(uThreshold)、平滑宽度(uSmoothEdge);
  3. 画一个铺满视口的四边形(两个三角形);
  4. 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 如何贯穿全链路

这是整条链路"零拷贝"的核心:

  1. VPU 输出 NV12 即是一个 dmabuf fd(MppBuffer,基于 DMA-BUF/ION,见《MPP 硬解码》2.4 节);
  2. RGA 导入该 dmabuf ,把 NV12 转 RGBA,写出到 GBM 分配的 dmabuf(RGA 支持 dmabuf 输入输出);
  3. EGL 用 EGL_EXT_image_dma_buf_import 把 GBM/dmabuf 包装成 EGLImageKHR ,再 glEGLImageTargetTexture2DOES 绑定为 GL 纹理------无需把像素搬回 CPU;
  4. Shader 合成结果直接写回另一块 GBM/dmabuf;
  5. 该 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)上的视频合成、虚拟演播、直播抠像与商业显示产品,可直接作为嵌入式图形工程的参考实现。


参考资料

  1. 《MEDAI-W2 拉流解码器使用说明书》(固件版本 2.1,文档版本 V1.0)
  2. 《MEDAI-W2 UVC 输入 × MPP 硬解码》技术解析(同系列文章)
  3. 《MEDAI-W2 V4L2/UVC 框架与自动格式协商》技术解析(同系列文章)
  4. Linux Kernel Documentation: Documentation/gpu/drm-kms.rst、Documentation/gpu/drm-mm.rst
  5. Mesa / libdrm:gbm.h、xf86drmMode.h 与 EGL_EXT_image_dma_buf_import 扩展说明
  6. 瑞芯微 RK3518 平台技术资料(Mali-450、RGA、VOP/HDMI 章节)
相关推荐
智能直播3 小时前
MEDAI-W2_FFmpeg多协议归一拉流_CSDN文章
视频编解码
智能直播3 小时前
MEDAI-W2_Tinyalsa音频_HDMI输出_CSDN文章
音视频·实时音视频·视频编解码
海水冷却1 天前
在线视频聊天:实时沟通正在发生怎样的变化
实时音视频
voipmaker2 天前
GB28181 对接踩坑实录:注册不上、目录不全、流卡顿,一次说清(附 2022 版变更清单)
物联网·音视频·实时音视频·信息与通信·iot·gb28181·国标gb28181
海水冷却2 天前
RTC+AI实时翻译:打破语言障碍的音视频通话方案
实时音视频
shxjnpl3 天前
从 WenetSpeech-Yue 到 SenseVoice:如何通过专项微调提升粤语语音识别效果?
人工智能·语音识别·实时音视频
shxjnpl5 天前
粤语客服录音如何实现自动质检?从 ASR 语音识别到大模型分析
人工智能·语音识别·实时音视频
事圆则缓5 天前
Flutter 与原生交互的最后一层:RTC 视频帧怎么走
flutter·音视频·实时音视频
shxjnpl5 天前
粤语医患对话如何转成电子病历?从方言 ASR 到端侧离线语音处理
人工智能·语音识别·实时音视频