『7x24小时有问必答』
嵌入式芯片现在都带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 框架总览:两层分离

这套跨平台框架分两层,互不依赖:
层级
职责
核心库
渲染层
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项目用了一套六步渐进式Demo设计,每一步在前一步基础上叠加一个新特性:
通过命令行参数切换:
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不是偷懒,每一步都是独立可运行的完整程序,组合起来又是完整的渲染引擎。

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

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

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

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

三个设计决策串起整个架构:
策略模式——光照计算。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渲染引擎,都能搞定。
</combinedrenderer></combinedrenderer></trianglerenderer></vector></cstdio></gles3></egl>

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码