|
嵌入式芯片现在都带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> 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |