ESC
输入关键词搜索文章标题和内容

嵌入式GPU+OpenGLES跨平台框架:一套代码通吃主流平台和PC的EGL离屏渲染方案

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

嵌入式GPU+OpenGLES跨平台框架:一套代码通吃主流平台和PC的EGL离屏渲染方案

导读:嵌入式芯片现在都带GPU和NPU了,异构计算已是标配。但GPU在嵌入式上怎么玩?OpenGL ES怎么跨平台?本文从零搭一套EGL离屏渲染框架,通吃主流嵌入式平台和PC,含渐进式Demo和核心架构拆解。

一、从单核CPU到异构计算

现在嵌入式SoC全是异构计算——CPU管逻辑、GPU管渲染、NPU管AI推理。驱动GPU的统一API是OpenGL ES 3.0+,不管哪家GPU(ARM Mali、Broadcom VideoCore、Vivante GC),接口一致,一套代码通吃:

版本 关键特性 典型场景
OpenGL ES 2.0 可编程着色器、FBO 简单2D/3D UI
OpenGL ES 3.0 MRT、浮点纹理、UBO 延迟渲染、HDR
OpenGL ES 3.1 计算着色器、间接绘制 GPU粒子系统
OpenGL ES 3.2 Geometry/Tessellation Shader 地形细分、曲面渲染

四核GPU做到400+ GFLOPS已经很普遍,做工业HMI、机器人仿真、AR导航都够用。

二、一套环境通吃所有平台

2.1 框架总览:两层分离

flowchart TB subgraph 渲染层["渲染层(GPU离屏)"] EGL["EGL Surfaceless<br/>Display + Context"] GLES["OpenGL ES 3.0<br/>着色器 + 渲染管线"] PB["Pbuffer Surface<br/>GPU内存离屏缓冲"] EGL --> GLES --> PB end subgraph 显示层["显示层(CPU贴图)"] SDL["SDL2<br/>窗口 + 输入事件"] BLIT["SDL_BlitScaled<br/>像素贴图"] SDL --> BLIT end PB -->|"glReadPixels<br/>GPU→CPU"| BLIT style 渲染层 fill:#E3F2FD,stroke:#1976D2 style 显示层 fill:#E8F5E9,stroke:#388E3C

这套跨平台框架分两层,互不依赖:

层级 职责 核心库
渲染层 GPU离屏渲染,输出像素数据 EGL + OpenGL ES
显示层 窗口管理、像素贴图、输入事件 SDL2

EGL只管画,SDL2只管贴——未来换Qt或FLTK做窗口,只改显示层,渲染层一行不动。

2.2 核心依赖

不管板子跑的是Armbian、Debian、Yocto还是Buildroot,核心依赖就这几样:

依赖库 作用 说明
libEGL EGL窗口系统接口 管理渲染上下文和窗口Surface的绑定
libGLESv2 OpenGL ES 3.0运行时 提供 gl3.h 头文件和着色器编译链接
SDL2 跨平台窗口管理 创建窗口、处理输入事件,不参与GPU渲染
libdrm + libgbm DRM/GBM后端 无头渲染时管理GPU缓冲区分配
Wayland(可选) 显示协议 如果你的系统跑Wayland而不是X11

特别注意:使用Mali闭源驱动的平台,Mesa的EGL库会被芯片厂商提供的libmali.so替代。但API接口完全一致——写代码时用标准头文件编译,运行时链接厂商的闭源库,这是嵌入式GPU开发的常规操作。

2.3 CMake配置(全平台通用)

cmake_minimum_required(VERSION 3.10)
project(opengles_demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

find_package(OpenGL REQUIRED)
find_package(EGL REQUIRED)
find_package(SDL2 REQUIRED)
find_package(PkgConfig REQUIRED)

pkg_check_modules(WAYLAND_CLIENT wayland-client)
pkg_check_modules(WAYLAND_EGL wayland-egl)

add_executable(opengles_demo
    src/main.cpp
    src/OffscreenRender.cpp
    src/SDLWindow.cpp
    src/renderer/TriangleRenderer.cpp
)

target_link_libraries(opengles_demo
    OpenGL::GLES3
    ${EGL_LIBRARIES}
    ${SDL2_LIBRARIES}
    ${WAYLAND_CLIENT_LIBRARIES}
    ${WAYLAND_EGL_LIBRARIES}
    pthread
)

调试心法:嵌入式平台交叉编译时,设置CMAKE_SYSROOT指向交叉编译工具链的系统根目录,Mesa和SDL2的.cmake模块会自动搜索。

三、最简单的OpenGL ES Demo:从三角形到3D场景

OpenGL ES的"Hello World"——一个三角形的EGL离屏渲染。不到100行代码,走完完整流程。

// triangle_egl.cpp - 最简EGL离屏渲染三角形
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <cstdio>
#include <vector>

// 顶点着色器
const char* vertexShaderSource = R"(#version 300 es
    in vec3 aPos;
    void main() {
        gl_Position = vec4(aPos, 1.0);
    }
)";

// 片段着色器
const char* fragmentShaderSource = R"(#version 300 es
    out vec4 FragColor;
    void main() {
        FragColor = vec4(1.0, 0.35, 0.0, 1.0);  // 橙色
    }
)";

int main() {
    // 1. 创建EGL Surfaceless Display
    PFNEGLGETPLATFORMDISPLAYPROC eglGetPlatformDisplay =
        (PFNEGLGETPLATFORMDISPLAYPROC)eglGetProcAddress("eglGetPlatformDisplay");
    EGLDisplay display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
                                                EGL_DEFAULT_DISPLAY, NULL);
    if (display == EGL_NO_DISPLAY) {
        printf("❌ 无法创建EGL Display\n");
        return -1;
    }

    // 2. 初始化EGL
    EGLint major, minor;
    eglInitialize(display, &major, &minor);
    printf("✅ EGL版本: %d.%d\n", major, minor);

    // 3. 选择EGL配置
    EGLint configAttribs[] = {
        EGL_SURFACE_TYPE,    EGL_PBUFFER_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
        EGL_RED_SIZE,   8, EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE,  8, EGL_ALPHA_SIZE, 8,
        EGL_DEPTH_SIZE, 24,
        EGL_NONE
    };
    EGLConfig config;
    EGLint numConfigs;
    eglChooseConfig(display, configAttribs, &config, 1, &numConfigs);

    // 4. 创建Pbuffer离屏表面
    EGLint pbufferAttribs[] = {
        EGL_WIDTH,  800, EGL_HEIGHT, 600, EGL_NONE
    };
    EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs);

    // 5. 创建OpenGL ES 3.0上下文
    EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
    EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
    eglMakeCurrent(display, surface, surface, context);

    printf("✅ OpenGL ES版本: %s\n", glGetString(GL_VERSION));
    printf("✅ GPU渲染器: %s\n", glGetString(GL_RENDERER));

    // 6. 编译着色器
    auto compileShader = [](GLenum type, const char* src) -> GLuint {
        GLuint shader = glCreateShader(type);
        glShaderSource(shader, 1, &src, NULL);
        glCompileShader(shader);
        GLint compiled;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
        if (!compiled) {
            char log[512]; glGetShaderInfoLog(shader, 512, NULL, log);
            printf("❌ 着色器编译失败: %s\n", log);
        }
        return shader;
    };

    GLuint program = glCreateProgram();
    glAttachShader(program, compileShader(GL_VERTEX_SHADER, vertexShaderSource));
    glAttachShader(program, compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource));
    glLinkProgram(program);

    // 7. 三角形顶点数据
    float vertices[] = {
         0.0f,  0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f
    };
    GLuint vao, vbo;
    glGenVertexArrays(1, &vao);
    glGenBuffers(1, &vbo);
    glBindVertexArray(vao);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // 8. 渲染
    glViewport(0, 0, 800, 600);
    glClearColor(0.1f, 0.1f, 0.15f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);
    glBindVertexArray(vao);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    // 9. 读取像素
    std::vector<unsigned char> pixels(800 * 600 * 4);
    glReadPixels(0, 0, 800, 600, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
    printf("✅ 渲染完成,800x600 RGBA像素已就绪\n");

    // 10. 清理
    eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    eglDestroyContext(display, context);
    eglDestroySurface(display, surface);
    eglTerminate(display);
    return 0;
}

这10步走完,GPU在离屏缓冲区里渲染了一个橙色三角形,像素数据通过glReadPixels读到内存。没有窗口,没有显示器,GPU照样干活。

避坑:EGL_PLATFORM_SURFACELESS_MESA的值是0x31DD,如果Mesa头文件里没有定义这个宏,手动#define即可。在Mali闭源驱动中,这个扩展同样可用。

渐进式Demo:从三角形到完整3D场景

opengles_wayland项目(GitHub: linuxros/opengles_wayland)用了一套六步渐进式Demo设计,每一步在前一步基础上叠加一个新特性:

flowchart TD M1["Mode 1: Triangle<br/>最简三角形"] --> M2["Mode 2: Cube Orbit<br/>旋转立方体+轨道相机"] M2 --> M3["Mode 3: Cube+Skybox<br/>天空盒+无限远背景"] M3 --> M4["Mode 4: Ground+Light<br/>地面网格+方向光"] M4 --> M5["Mode 5: Full PBR<br/>PCF阴影+Blinn-Phong"] M5 --> M6["Mode 6: 3D Model<br/>Assimp模型+骨骼动画"] style M1 fill:#E3F2FD,stroke:#1976D2 style M2 fill:#E3F2FD,stroke:#1976D2 style M3 fill:#E3F2FD,stroke:#1976D2 style M4 fill:#E8F5E9,stroke:#388E3C style M5 fill:#FFF8E1,stroke:#F57C00 style M6 fill:#F3E5F5,stroke:#7B1FA2

通过命令行参数切换:

int mode = std::atoi(argv[1]);
if (mode == 1) {
    renderer = std::make_unique<TriangleRenderer>();
} else if (mode == 5) {
    auto combined = std::make_unique<CombinedRenderer>(&window, &render, camera);
    combined->setLight(light.get());
    combined->setLightingStrategy(strategy.get());
    combined->setShadowMap(shadowMap.get());
    renderer = std::move(combined);
}

Mode 5的阴影有问题?回退到Mode 4确认光照,再回退到Mode 3确认天空盒,逐层排查,不会被一堆代码淹没。

渐进式Demo不是偷懒,每一步都是独立可运行的完整程序,组合起来又是完整的渲染引擎。

来自 linuxros.cn · linuxROS

四、核心架构拆解:EGL Surfaceless + SDL2贴图显示

4.1 数据流:GPU离屏渲染 → 屏幕显示

flowchart LR A["EGL Surfaceless<br/>Display"] --> B["Pbuffer<br/>Surface"] B --> C["OpenGL ES 3.0<br/>Context"] C --> D["GPU离屏渲染"] D --> E["glReadPixels"] E --> F["像素数据<br/>RGBA"] F --> G["SDL2窗口显示<br/>SDL_BlitScaled"] F --> H["保存PNG截图"] style A fill:#E3F2FD,stroke:#1976D2 style B fill:#E3F2FD,stroke:#1976D2 style C fill:#E3F2FD,stroke:#1976D2 style D fill:#F3E5F5,stroke:#7B1FA2 style E fill:#FFF8E1,stroke:#F57C00 style F fill:#E8F5E9,stroke:#388E3C style G fill:#E8F5E9,stroke:#388E3C style H fill:#E8F5E9,stroke:#388E3C

整个链路分两段:前段EGL离屏渲染,GPU在Pbuffer里画完,glReadPixels把像素读到CPU内存;后段SDL2做纯软件贴图,SDL_BlitScaled把像素往窗口Surface上贴,完全不碰GPU。这个拆分的好处是EGL和SDL2互不依赖——EGL只管渲染,SDL2只管显示,未来换Qt或FLTK做窗口,只需要改后段。

4.2 接口层设计:策略模式 + 依赖注入

flowchart TB subgraph 接口层 IR["IRenderer 渲染器接口"] IW["IWindow 窗口接口"] IO["IOffscreenRender 离屏渲染接口"] ILS["ILightingStrategy 光照策略接口"] IL["ILightSource 光源接口"] ISM["IShadowMap 阴影映射接口"] end subgraph 实现层 CR["CombinedRenderer 综合渲染器"] MR["ModelRenderer 模型渲染器"] SW["SDLWindow SDL2窗口"] OR["OffscreenRender EGL离屏渲染"] BP["BlinnPhongStrategy Blinn-Phong光照"] DL["DirectionalLight 方向光"] PS["PCFShadowMap PCF阴影"] end IR --> CR IR --> MR IW --> SW IO --> OR ILS --> BP IL --> DL ISM --> PS style 接口层 fill:#E3F2FD,stroke:#1976D2 style 实现层 fill:#E8F5E9,stroke:#388E3C

三个设计决策串起整个架构:

策略模式——光照计算。ILightingStrategy定义光照计算接口,BlinnPhongStrategy是具体实现。想换成PBR的Cook-Torrance模型?新增一个策略类,CombinedRenderer和ModelRenderer完全不用改。

依赖注入——CombinedRenderer不自己创建光源、阴影、光照策略,而是通过setLight()、setShadowMap()、setLightingStrategy()接收外部注入:

auto combined = std::make_unique<CombinedRenderer>(&window, &render, camera);
combined->setLight(light.get());              // 注入光源
combined->setLightingStrategy(strategy.get()); // 注入光照策略
combined->setShadowMap(shadowMap.get());       // 注入阴影映射

接口隔离——每个渲染器只依赖它需要的接口。TriangleRenderer只需要IRenderer,CombinedRenderer需要IRenderer和IShadowCaster(参与阴影映射的深度渲染)。

4.3 项目目录结构

project/
├── include/          # 接口层:纯虚接口,定义渲染器/窗口/离屏/光照/阴影的抽象契约
├── src/              # 实现层:EGL离屏渲染、SDL2窗口、依赖注入组装
│   └── renderer/     # 渲染器实现:Triangle → Combined → Model 六步渐进
└── CMakeLists.txt

这个架构的通用性在两方面:跨芯片平台和跨渲染后端。EGL换Vulkan的VkSurface,SDL2换Qt的QOpenGLWidget,接口层不变,只换实现层。

五、常见问题解决

EGL初始化失败,eglGetPlatformDisplay返回EGL_NO_DISPLAY

检查Mesa版本是否 ≥ 18.0。如果Mali闭源驱动不支持Surfaceless扩展,回退到标准eglGetDisplay(EGL_DEFAULT_DISPLAY)创建Pbuffer——离屏渲染的核心逻辑不受影响。

SDL2窗口显示画面撕裂或闪烁

SDL_GetWindowSurface不会自动双缓冲。在调用SDL_BlitScaled之前,先用SDL_FillRect清空窗口Surface,避免上一帧的残留像素和新画面重叠。

交叉编译时CMake找不到EGL/SDL2

find_package在交叉编译时默认搜索宿主机的库路径。设置CMAKE_SYSROOT指向目标文件系统的根目录,同时设置CMAKE_FIND_ROOT_PATH_MODE为ONLY,强制CMake只在sysroot内搜索。

glReadPixels读出来的颜色和渲染结果不一致

glReadPixels默认从帧缓冲的左下角开始读取,而大多数图像格式从左上角开始。如果保存的截图上下颠倒,在读取后用std::reverse翻转行数据。

六、总结

嵌入式GPU上跑OpenGL ES,三件事:EGL管上下文,Surfaceless管离屏,接口隔离管架构。

  • Demo阶段:直接写EGL+GL代码,100行出一个三角形,适合验证原型
  • 项目阶段:上接口隔离架构,接口层/实现层/组装层三层分离,换平台只换实现、换后端只加策略
  • 跨平台:EGL API统一了芯片厂商的闭源驱动和PC的Mesa开源驱动,一套代码全部通吃

EGL Surfaceless + SDL2 + 策略模式,从简单三角形一路到带骨骼动画的完整3D渲染引擎,都能搞定。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页