3、窗口与上下文管理:窗口创建与事件循环、图形上下文初始化(OpenGL/WGL/EGL)、多窗口与多线程上下文、平台差异处理
窗口和上下文管理,说白了就是图形子系统的「地基」。地基没打好,后面渲染再漂亮也白搭。我这些年调试过的图形问题,至少有一半都能追溯到上下文创建或事件循环的某个细节上。
3.1 窗口创建与事件循环
窗口创建这件事,不同平台有不同 API,但核心逻辑是一致的:
- 注册窗口类(Windows)或创建 NSWindow(macOS)
- 设置窗口属性(大小、标题、样式)
- 进入事件循环,处理消息
我个人习惯把窗口创建和事件循环封装成一个抽象层。举个例子,Windows 平台下典型的窗口循环长这样:
// Windows 消息循环
MSG msg = {0};
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
嗯,这里要注意——PeekMessage 和 GetMessage 的区别。前者是非阻塞的,适合游戏循环;后者会阻塞直到有消息。我在项目中遇到过用 GetMessage 做实时渲染,结果窗口一最小化,渲染线程直接卡死。避坑指南:实时渲染用 PeekMessage + WaitMessage 组合。
3.2 图形上下文初始化
图形上下文是 GPU 和应用程序之间的「通道」。OpenGL、WGL、EGL 三者的关系,我画了张图:
3.2.1 WGL 初始化(Windows)
Windows 下创建 OpenGL 上下文的经典流程:
// 1. 创建窗口(需要 WS_CLIPCHILDREN | WS_CLIPSIBLINGS 风格)
HWND hwnd = CreateWindowEx(0, className, title,
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
100, 100, 800, 600, NULL, NULL, hInstance, NULL);
// 2. 获取设备上下文
HDC hdc = GetDC(hwnd);
// 3. 设置像素格式
PIXELFORMATDESCRIPTOR pfd = {
sizeof(PIXELFORMATDESCRIPTOR), 1,
PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA, 32, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 24, 8, 0, PFD_MAIN_PLANE, 0, 0, 0, 0
};
int pf = ChoosePixelFormat(hdc, &pfd);
SetPixelFormat(hdc, pf, &pfd);
// 4. 创建 OpenGL 上下文
HGLRC hglrc = wglCreateContext(hdc);
wglMakeCurrent(hdc, hglrc);
wglCreateContext 只能创建 OpenGL 1.x 上下文。要使用现代 OpenGL(3.2+),必须用 wglCreateContextAttribsARB。我见过太多人卡在这一步,创建出来的上下文最高只支持 OpenGL 2.1。
3.2.2 EGL 初始化(嵌入式/移动端)
EGL 是 OpenGL ES 的标准上下文管理接口。Android 和 Linux 嵌入式设备都在用:
// 1. 获取 EGL 显示连接
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, NULL, NULL);
// 2. 选择配置
EGLConfig config;
EGLint configAttribs[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_NONE
};
EGLint numConfigs;
eglChooseConfig(display, configAttribs, &config, 1, &numConfigs);
// 3. 创建窗口表面
EGLSurface surface = eglCreateWindowSurface(display, config, nativeWindow, NULL);
// 4. 创建上下文
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, NULL);
eglMakeCurrent(display, surface, surface, context);
你想想看,EGL 和 WGL 最大的区别在哪?EGL 把「显示连接」和「窗口表面」分开了。这意味着你可以用一个 EGLDisplay 管理多个窗口表面。我在嵌入式项目中就用这个特性做过双屏异显——一个屏显示主界面,另一个屏显示仪表盘。
3.3 多窗口与多线程上下文
多窗口多线程是图形子系统里最容易出 bug 的地方。核心原则就一条:一个上下文只能被一个线程在任意时刻使用。
| 场景 | 上下文策略 | 注意事项 |
|---|---|---|
| 单窗口单线程 | 1 个上下文 | 最简单,无竞态 |
| 多窗口单线程 | 每个窗口 1 个上下文,轮流 MakeCurrent |
切换开销大,注意释放旧上下文 |
| 单窗口多线程 | 1 个共享上下文 + 每个线程 1 个辅助上下文 | 共享列表(纹理、VBO)需同步 |
| 多窗口多线程 | 每个窗口 1 个主上下文 + 共享上下文池 | 最复杂,建议用队列模型 |
我曾经在交易系统项目里踩过一个坑:两个线程各自创建了上下文,但共享了同一个纹理 ID。结果一个线程在删除纹理时,另一个线程正在使用它——直接崩溃。后来我加了一个引用计数管理器,才彻底解决。
- 创建主上下文时指定
wglCreateContextAttribsARB的共享列表参数 - 工作线程创建辅助上下文,与主上下文共享显示列表和纹理对象
- 使用
glFenceSync或glFinish保证同步 - 绝对不要在多个线程中同时操作同一个上下文
3.4 平台差异处理
不同平台的上下文管理差异,说白了就是「窗口系统不同,桥接方式不同」。我整理了一张对比表:
| 平台 | 上下文接口 | 窗口系统 | 典型坑点 |
|---|---|---|---|
| Windows | WGL | Win32 / WinRT | 像素格式只能设置一次;DWM 合成影响 SwapBuffers 时机 |
| Linux (X11) | GLX | Xlib / XCB | 多显示器下 GLXFBConfig 选择复杂;X 连接线程安全需注意 |
| Linux (Wayland) | EGL | wl_surface | 没有全局坐标;窗口 resize 需要重新创建 EGLSurface |
| Android | EGL | SurfaceFlinger | ANativeWindow 生命周期与 Activity 绑定;后台时上下文会丢失 |
| macOS | NSOpenGL / CGL | NSWindow + CoreAnimation | 必须主线程操作 AppKit;OpenGL 已废弃,推荐 Metal |
嗯,这里有个容易被忽略的点:上下文丢失。移动端和嵌入式设备上,当应用切到后台,EGL 上下文可能被系统销毁。我见过一个团队在 Android 上做实时渲染,切到后台再切回来,纹理全部变成黑色方块——就是因为没有监听 onPause/onResume 重建上下文。
- 抽象一个
GraphicsContext类,封装MakeCurrent、SwapBuffers、Destroy - 平台差异用工厂模式隔离,不要写
#ifdef满天飞 - 上下文创建参数(颜色位数、深度位数、采样数)统一用结构体传递
- 多线程场景下,用
std::thread::id做上下文绑定校验,调试时能省大量时间
最后说一句:上下文管理看似基础,但它是整个图形子系统的「心脏」。我每次接手一个新项目,第一件事就是看上下文创建和事件循环的代码——这里如果写得乱,后面渲染管线再漂亮也跑不稳。