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 组合。

经验之谈: 跨平台窗口库(GLFW、SDL)虽然方便,但如果你需要深度控制(比如子窗口嵌入、透明窗口),还是得自己封装。我曾经在金融终端项目里被迫手写 Win32 窗口管理,因为 GLFW 不支持窗口透明混合。

3.2 图形上下文初始化

图形上下文是 GPU 和应用程序之间的「通道」。OpenGL、WGL、EGL 三者的关系,我画了张图:

图形上下文初始化架构 应用程序 OpenGL / OpenGL ES API WGL (Windows) EGL (嵌入式/移动) GLX (Linux/X11) GPU 驱动 / 窗口系统驱动 关键点: WGL/EGL/GLX 负责将 OpenGL 命令桥接到具体窗口系统和 GPU 驱动,上下文创建顺序:窗口 → 显示连接 → 配置选择 → 上下文创建 → 绑定。

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。结果一个线程在删除纹理时,另一个线程正在使用它——直接崩溃。后来我加了一个引用计数管理器,才彻底解决。

多线程上下文共享的正确姿势:
  1. 创建主上下文时指定 wglCreateContextAttribsARB 的共享列表参数
  2. 工作线程创建辅助上下文,与主上下文共享显示列表和纹理对象
  3. 使用 glFenceSync 或 glFinish 保证同步
  4. 绝对不要在多个线程中同时操作同一个上下文

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 做上下文绑定校验,调试时能省大量时间

最后说一句:上下文管理看似基础,但它是整个图形子系统的「心脏」。我每次接手一个新项目,第一件事就是看上下文创建和事件循环的代码——这里如果写得乱,后面渲染管线再漂亮也跑不稳。