简介:在Windows图形编程中,GDI与OpenGL分别代表了CPU软绘制与GPU硬件加速两条技术路径。GDI擅长线条、文字等基础2D绘制,而OpenGL通过渲染管线和着色器实现复杂3D场景与高效图形输出。理解二者在像素格式、渲染上下文、双缓冲交换等底层机制上的差异,是构建混合渲染架构的关键。实际桌面应用中,常以GDI实现HUD、坐标轴等覆盖层,用OpenGL承载主场景渲染,兼顾开发效率与性能表现。本文以“OpenGL-Drawing.rar”示例工程为切入点,从工程配置、固定管线代码拆解到glUniformMatrix4fv矩阵上传、线段粗细控制等高频问题,系统梳理Visual C++环境下让老代码在新系统稳定运行的方法,为入门混合绘图提供完整参考。 拿到“OpenGL-Drawing.rar_GDI图象编程_Visual C++_”这个压缩包的时候,我第一反应是:这八成又是一个编程学习路上的经典示例工程。名字里同时带着OpenGL、GDI、Visual C++三个关键词,某种意义上就是Windows图形编程的两条技术路线被塞进了同一个项目里。很多人会拿它当C++课后作业的参考,也有人是冲着OpenGL的入门代码来的,还有人是想看看GDI和OpenGL到底能不能在同一个窗口里共存。这个包的价值不在于它有多大、多新,而在于它把Windows下两种最典型的绘图方式放在一起,适合做对照学习,也适合在Visual C++环境里直接编译跑起来体验效果。
这类示例工程对应的核心场景,是Windows桌面应用里的图像绘制和渲染。GDI负责传统的2D图形绘制,比如画线、画矩形、文字输出,走的是CPU,简单直接;OpenGL则走GPU管线,能处理3D场景、复杂光照、纹理映射,也可以用来加速2D渲染。两者在同一个程序里搭配使用,属于很经典的“界面用GDI,渲染用OpenGL”的混合架构。这篇文章我会从解压这个包开始,把它涉及的工程配置、关键代码、常见运行问题全部拆开讲一遍,帮你在Visual C++环境下把OpenGL绘图跑起来,并理解每一段代码背后的原因。
1. 从压缩包看项目:OpenGL与GDI并存的设计思路
1.1 解压后应该看到什么
如果你手头有这个压缩包,解压后大概率会看到一个Visual Studio工程,里面包含.dsp或者.vcxproj工程文件、.cpp和.h源文件,可能还有.rc资源文件。这类项目在VC6时代最流行,后来被移植到VS2008、VS2010乃至更高版本也很常见。压缩包文件名里出现“RAR”说明是老的打包方式,里面的代码可能带着上世纪九十年代到二十一世纪初的风格,比如glBegin()、glEnd()这种已经过时但在教学里依然好用的固定管线API。
打开源文件,你会发现工程的基本结构无外乎这几部分:一个Win32窗口类,用于创建主窗口和处理消息;一个OpenGL渲染上下文初始化模块,负责把窗口和OpenGL绑定到一起;一个绘制函数,里面放着实际的GL调用;以及可能存在的GDI绘图代码,比如用TextOut()显示FPS,用MoveToEx()和LineTo()画辅助线。这种结构放在今天看依然值得学习,因为它把“窗口系统相关”和“渲染相关”的代码做了清晰分层。
1.2 为什么同时涉及OpenGL和GDI
很多初学者看到这个工程里既有OpenGL又有GDI,会觉得疑惑:两个绘图系统混在一起,不是给自己找麻烦吗?实际上,在真实项目里这种混合非常普遍。Windows窗口本身的消息机制、标题栏、按钮这些基础UI元素,走的是系统自带的GDI或DirectComposition;而窗口客户区里的3D场景则由OpenGL接管。你不可能用OpenGL去画一个Windows原生按钮,也不应该用GDI去渲染一个带纹理贴图的3D模型。各管一段,互相配合,才是Windows图形编程的常态。
从学习角度讲,把OpenGL和GDI放在同一个工程里还有一个好处:你可以直观对比两种API的差异。GDI里画一个三角形,你需要MoveToEx()加两条LineTo();OpenGL里你只需要指定三个顶点,让显卡去填充。GDI的坐标以像素为单位,左上角为原点,Y轴向下为正;OpenGL的坐标是归一化的或者由你自己定投影矩阵,Y轴一般向上为正。这些差异不看实际代码很难真正形成肌肉记忆。所以这个压缩包真正的学习价值,不是一个“可以运行的OpenGL程序”,而是一份“GDI和OpenGL的对照样本”。
2. Visual C++图形编程的环境搭建与工程配置
2.1 编译器与运行库选择
在Visual C++环境下跑这种老工程,第一步遇到的往往是编译器兼容性问题。老的.dsp工程用VC6编译时只认Windows SDK的老版本头文件和库文件;如果你用Visual Studio 2022打开,VS的升级向导会尝试把工程转换成新格式,但转换后经常出现一堆链接错误。我的建议是:不要纠结于把老工程原封不动编译通过,而是新建一个空的Win32项目,把.cpp和.h文件拷贝进去,再手动配置OpenGL链接项。这样既保留原有代码,又规避工程格式兼容性问题。
选择哪个Visual Studio版本也有讲究。如果你只是跑通OpenGL示例,Visual Studio 2019或2022社区版完全够用,它们自带Windows SDK,也自带OpenGL的库和头文件。需要注意的只是“Visual C++运行库”这一项。系统里如果缺运行库,程序编译能通过,但双击exe运行时会弹窗提示“缺少VCRUNTIME140.dll”之类,或者安装一些工具时直接报0x80070666错误。这是老VC++开发者的老朋友了,解决方案也简单:去微软官网下载对应版本的“Visual C++ Redistributable”安装包。x86程序装x86运行库,x64程序装x64运行库,别混着装。
2.2 链接OpenGL库与包含头文件
在Visual Studio里配置OpenGL,实际上不需要你手动下载任何额外的东西。Windows SDK自带OpenGL 1.1的头文件和导入库,你只需要在“项目属性 - 链接器 - 输入 - 附加依赖项”里加上opengl32.lib和glu32.lib,并在源代码里包含<windows.h>、<GL/gl.h>、<GL/glu.h>。很多新手在这一步犯错,是因为用了#include <GL/glut.h>这种GLUT头文件,而工程里又没有链接glut32.lib或freeglut.lib,导致一堆“无法解析的外部符号”。这种老示例代码如果没提GLUT,你千万别自己加,先按原代码来。
链接库这一步做完之后,还有一个容易被忽视的点:如果你在较新版本的Windows SDK下编译,需要确保工程的“字符集”设置和代码匹配。老代码里用char数组的WinMain,如果工程默认设成Unicode,会导致入口点、消息处理函数这两类的函数签名对不上,编译报错。我处理这类老代码的习惯是,在工程属性里把“字符集”改为“使用多字节字符集”,然后重新编译。这个操作现在VS里已经默认隐藏了,你需要在“配置属性 - 高级”里找到“字符集”选项手动切换。
2.3 编译链路中的常见运行库问题
搞Windows C++开发的,十有八九撞过运行库相关的妖。有人装Node.js时报“Microsoft Visual C++ 2022 x86 Minimum Runtime安装包不存在”,有人装Git时提示“TortoiseGit需要Visual C++”,还有人同一台机器上装了Visual Studio 2015到2022的好几套Redistributable仍然被某个软件提示缺库。这里面有个规律:不是缺运行库,而是缺特定架构和特定版本的运行库。比如64位电脑上跑32位老程序,你需要的是x86版本运行库,只装x64版本不够。
运行库问题在编译这类OpenGL示例时还体现在另一个层面:opengl32.lib导入库本身的兼容性。老示例可能引用了一些老函数,在最新Windows SDK中对应导入库依然存在,链接一般不会出问题。真正出问题的是glu32.lib里的gluBuild2DMipmaps()这类函数——不是所有SDK版本都保证导出。遇到不导出时,有两个选择:改代码用glGenerateMipmap()(需要OpenGL 1.4以上),或者自己手动创建多级纹理。我通常建议直接改代码,因为glu*这套工具库本来就是可选的,能不用就不用。
3. 核心绘图代码拆解:从初始化到绘制
3.1 像素格式设置与渲染上下文创建
一个OpenGL程序能在Windows窗口上画图,靠的不是直接调用glClear(),而是先把这个窗口和OpenGL渲染上下文绑定起来。绑定的第一步是设置像素格式。这段代码通常在WM_CREATE消息处理里,核心函数是ChoosePixelFormat()和SetPixelFormat()。你要定义一个PIXELFORMATDESCRIPTOR结构体,填上颜色位数、深度位数、是否双缓冲这些参数,然后让Windows帮你找到最匹配的像素格式并设置到当前设备上下文(DC)上。
很多老示例的像素格式设置只有一份,里面dwFlags同时写上PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,颜色位数设为32,深度位数设为24或32。这个配置到今天依然能跑。但我得提醒一句:老代码里常有PFD_GENERIC_ACCELERATED和PFD_GENERIC_FORMAT这些标志位混用的情况,在某些显卡驱动下会导致ChoosePixelFormat()返回一个软件渲染的像素格式,后续渲染自然全部走CPU模拟,性能和效果都大打折扣。如果遇到渲染明显偏慢、GPU占用却为零的情况,优先排查你设置的像素格式是不是被驱动识别成了软件格式。
像素格式设置好之后,第二步是调用wglCreateContext()创建渲染上下文,再wglMakeCurrent()把它绑定到当前线程。这一步完成后,你才可以在该线程里调用任何gl*函数。这里有个关键点:OpenGL渲染上下文是跟当前线程绑定的,不是跟窗口绑定的。多线程渲染时需要为每个线程创建独立的上下文,或者共享同一个上下文资源,绝对不能在两个线程里同时用同一个上下文做渲染。
3.2 固定管线绘制流程与核心函数
老式OpenGL代码的核心逻辑往往是这样的:在窗口尺寸变化时用glViewport()设置视口;在渲染函数里先glClear()清空颜色缓冲和深度缓冲,然后glMatrixMode(GL_MODELVIEW)、glLoadIdentity()重置矩阵;再通过gluLookAt()或glTranslatef()、glRotatef()设置相机和模型变换;接着用glBegin()、glColor3f()、glVertex3f()、glEnd()提交顶点绘制图元;最后SwapBuffers()交换双缓冲,让画面显示到窗口上。
这套固定管线写起来简单直观,适合教学,但从现代OpenGL视角看已经非常陈旧。它的问题是:状态太多、驱动优化困难、无法利用可编程管线的灵活性。如果你只是验证示例效果,固定管线没问题;如果你打算做正式项目,建议把学习重点放到可编程管线上——用glCreateShader()、glCompileShader()、glLinkProgram()、glUniformMatrix4fv()这套流程。不过这个老示例的价值,恰恰是让你先通过固定管线理解“OpenGL就是一个画三角形的库”,然后再去学习现代写法。
3.3 glUniformMatrix4fv用法详解
glUniformMatrix4fv()在现代OpenGL里几乎每帧都要用,它负责把CPU端计算好的4x4矩阵传给GPU着色器里的uniform变量。很多刚接触可编程管线的朋友第一次看到这个函数,会对它的参数一头雾水。函数签名是这样的:
void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);第一个参数是uniform变量的location,通过glGetUniformLocation(program, "uMatrix")获取;第二个参数count代表要上传几个矩阵,通常写1;第三个参数transpose指定矩阵是否需在GPU端转置,这个坑最大——OpenGL以列主序存储矩阵,而很多数学库(比如glm)默认是列主序但行为可能让你困惑,实践中绝大多数人写GL_FALSE,然后用列主序的数组传递矩阵;第四个参数就是矩阵数据的指针。
用的时候经常会遇到两种情况:一种是矩阵没传上去,画面完全不动,原因多半是glGetUniformLocation()返回了-1(uniform被编译器优化掉了,因为着色器里没实际用到),调用glUniformMatrix4fv()时直接传-1会导致静默失败;另一种是矩阵上传后画面扭曲或整体错位,原因往往是transpose参数填了GL_TRUE,导致矩阵转置,投影或模型矩阵数值全错了。我自己的排查习惯是:先故意把matrix数据打印出来,和CPU端计算结果对比,确认CP端数值正确,再检查GPU着色器里的乘法顺序。
3.4 OpenGL线段粗细与绘图细节
热词里有人专门搜“OpenGL线段粗细”,说明这确实是个让新手头疼的问题。固定管线里画粗线用glLineWidth(),参数传的是像素单位的宽度,但OpenGL规范只保证支持1.0,超过1.0要看具体驱动实现。我在不少显卡上试过,glLineWidth(10.0f)调用后线段宽度纹丝不动,仍然是一个像素宽。原因在于驱动的支持上限,你可以用下面的代码查询:
GLfloat lineWidthRange[2] = {0.0f, 0.0f}; glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, lineWidthRange);查询结果里,第二个值就是当前驱动支持的最大线宽。如果最大宽度只有1.0或2.0,那很多显卡驱动里的宽线支持就是一纸空文。你想画粗线,别跟glLineWidth()死磕,正确方案是用三角形条带模拟线段:计算线段法线方向,生成两个顶点偏移,铺成矩形面片。这种方案也叫“膨胀线段”,在任何OpenGL版本下都稳定可用。
用OpenGL绘图时还有很多类似的“规范边界”问题。比如glColor3f()传的颜色取值是0.0到1.0的浮点范围,和GDI里0到255的整数范围容易搞混,两个系统代码混着用时经常出现颜色偏暗或反转的情况。再比如深度缓冲和绘制顺序:老示例里绘制线框时往往忘了关闭深度测试,导致线框被多边形遮挡;简单的对策是绘制线框前glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)配合glDisable(GL_DEPTH_TEST)。这些细节才是画好图的关键。
4. GDI与OpenGL的协同配合
4.1 用GDI做HUD,用OpenGL做场景
在同一个窗口里同时使用GDI和OpenGL,通常是因为OpenGL画起来费劲的内容有人想用GDI偷个懒。比如在3D场景上叠加一个带阴影的文字标签,用OpenGL实现需要创建纹理、绑定纹理、渲染带纹理的四边形,每一步都繁琐;而用GDI就简单得多,TextOut()一次调用搞定。但这里有个技术前提:你不能随便在OpenGL渲染的窗口上调用GDI绘图函数,否则画面会闪烁或者绘制内容直接被后续SwapBuffers()冲掉。
正确做法是:在OpenGL完成一帧渲染之后,立即在SwapBuffers()之前用GDI绘制,并且要获取与OpenGL渲染上下文共享的DC。具体来说,你在初始化时用GetDC(hwnd)拿到了DC,然后创建并绑定了OpenGL上下文,之后调用GDI绘制要用这同一个DC,而不是再获取一次。在整个绘制流程结束后再调用SwapBuffers(),这样GDI绘制的内容会和OpenGL内容一起呈现。如果你想用GDI绘制背景,还要设置好混合模式,否则背景会挡住OpenGL场景。实践下来,这种混合方式用于显示FPS、坐标轴标签、调试信息非常顺手,但用于复杂UI就不太合适了——Windows在现代版本里对GDI Overlay方式的优化几乎没有,复杂UI会拖慢整体渲染。
4.2 软渲染与GPU加速的判断
搜热词时我注意到一条高频搜索:“WSL Ubuntu GPU被识别了,但OpenGL渲染仍然在使用CPU软件模拟”。这个现象在Windows原生老示例里也存在,只是触发原因不同。在Windows原生环境下,OpenGL加载器(opengl32.dll)会根据像素格式自动决定走硬件加速还是软件渲染。如果你的像素格式里标志位PFD_GENERIC_ACCELERATED没被设置,或者显卡驱动不认这个格式,系统会回退到微软的GDI Generic OpenGL实现,也就是完全由CPU渲染。这种情况在虚拟机里最常见,在远程桌面会话里也会发生。
判断当前OpenGL是否为软件渲染,可以用glGetString(GL_RENDERER)查看渲染器名称。软件渲染的实现通常返回“GDI Generic”或“Microsoft Software Renderer”;硬件渲染则返回显卡型号和驱动名称,比如“NVIDIA GeForce RTX 3060”。如果你发现程序在软件渲染下跑,性能差是必然的——OpenGL软件模拟本身没有GPU加速,画一两个三角形能忍受,画几万个顶点就卡成PPT。解决思路是按顺序排查:像素格式配置是否正确、显卡驱动是否更新、是否运行在远程桌面会话里、是否被虚拟机限制。
4.3 双缓冲交换与画面撕裂
双缓冲几乎是所有OpenGL示例的默认配置。它的原理很简单:一个后缓冲用于GPU绘制,一个前缓冲用于屏幕显示,绘制完成后交换前后缓冲,避免用户看到“正在绘制一半”的画面。Windows下的交换函数是SwapBuffers(),Vulkan里叫vkQueuePresentKHR(),DXGI里叫Present(),概念完全一致。代码里如果你看到PFD_DOUBLEBUFFER标志,就说明用了双缓冲;没有这个标志的老示例可能存在画面闪烁问题,解决方法就是加上双缓冲。
跟双缓冲绑定的是垂直同步(VSync)问题。老示例不会自动启用垂直同步,所以在高刷新率屏幕上,画面会出现撕裂——屏幕一部分显示上一帧、另一部分显示新帧。解决撕裂的方案是启用垂直同步。在Windows里,可以用wglSwapIntervalEXT(1)启用垂直同步。这个函数不是OpenGL标准,实际是从WGL_EXT_swap_control扩展中拿到的,需要先获取扩展函数指针:
typedef BOOL (WINAPI *PFNWGLSWAPINTERVALEXTPROC)(int interval); PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT = (PFNWGLSWAPINTERVALEXTPROC)wglGetProcAddress("wglSwapIntervalEXT"); if (wglSwapIntervalEXT) wglSwapIntervalEXT(1);启用垂直同步后帧率会被显示器刷新率限制,比如60Hz显示器就锁在60FPS,画面不再撕裂,但如果你做性能测试,帧率会被限制而看不出真实性能。更好的做法是:做一个可切换的按键,比如按F1打开垂直同步,按F2关闭。
5. 常见问题与排查技巧实录
5.1 问题速查表
我把这些年跑老OpenGL示例时遇到的经典问题整理成一个表,方便你按症状快速定位。
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 编译报“无法解析的外部符号 _glClear@4” | 未链接opengl32.lib | 在工程属性里添加opengl32.lib、glu32.lib |
| 双击exe提示缺少VCRUNTIME140.dll | 缺少对应版本Visual C++运行库 | 安装对应架构的VC++ Redistributable |
| 画面黑屏但程序不崩溃 | 渲染上下文未创建或未绑定 | 检查wglCreateContext()和wglMakeCurrent()返回值 |
| 绘制结果全部是白色菱形 | 视口或投影矩阵设置错误 | 检查glViewport()和gluPerspective()调用参数 |
| 画面闪烁、有明显拖影 | 未启用双缓冲 | 像素格式加PFD_DOUBLEBUFFER,渲染后调用SwapBuffers() |
| 线段宽度无效 | 驱动不支持宽线 | 用三角形条带模拟粗线 |
| 渲染极慢且GPU占用为0 | 软件渲染回退 | 检查像素格式、驱动、远程会话环境 |
| 文字和3D画面重叠但文字闪烁 | GDI绘制时机不对 | GDI绘制放到SwapBuffers()之前 |
| 窗口缩放后画面拉伸变形 | 未处理WM_SIZE和glViewport | 在窗口尺寸变化时重设视口 |
| 颜色偏暗、偏灰 | 颜色范围用错 | GDI用0~255,OpenGL用0.0~1.0 |
5.2 老工程在新系统上的三大坑
第一个坑是DPI缩放。现代Windows默认对高分屏做DPI缩放,老程序如果不声明DPI感知,会被系统拉伸,导致OpenGL输出的画面变得模糊,甚至鼠标坐标和渲染坐标对不上。解决方案是在工程里添加一个应用程序清单文件,声明dpiAware为true,或者在程序入口调用SetProcessDPIAware()。这个操作在低分辨率屏幕上不明显,但在4K屏上会立刻感受到差别。
第二个坑是Windows消息循环的退出逻辑。老示例里经常用if (msg.message == WM_QUIT)来结束消息循环,但在现代Windows上,如果你在关闭主窗口时没有正确调用wglMakeCurrent(NULL, NULL)和wglDeleteContext(),OpenGL资源不会释放。多次在调试会话中打开关闭窗口后,会出现新的渲染上下文无法创建的错误。这不是OpenGL本身的问题,而是资源泄漏积累导致GDI对象耗尽。正确做法是在WM_DESTROY消息里依次执行:解除上下文绑定、删除上下文、释放DC、调用PostQuitMessage(0)。
第三个坑是glBegin()和glEnd()在现代核心Profile下的报错。OpenGL 3.2之后引入了Core Profile,固定管线函数被移除了。Windows系统本身支持最多OpenGL 4.6,但如果你创建的上下文是核心模式,调用glBegin()会直接报GL_INVALID_OPERATION或者什么都不画。老示例代码里的固定管线调用必须在兼容模式下才能运行。创建兼容上下文的方式因环境而异,大多数窗口库默认创建兼容上下文,但你在现代框架里用的时候要刻意指定。
5.3 实测追踪渲染问题的通用思路
如果你遇到OpenGL画面表现诡异,我强烈建议你养成“三步定位法”的习惯。第一步先确认上下文状态:在渲染函数入口调用glGetError()清空错误标记,再在函数末尾调用glGetError()查看是否出现新错误。OpenGL的错误是累积式的,一个glGetError()成功调用后,错误状态会被清除;连续几帧追踪,能快速锁定产生错误的调用点。第二步确认渲染状态:把glIsEnabled(GL_DEPTH_TEST)、glIsEnabled(GL_CULL_FACE)这些状态值打印出来,看有没有出现意料之外的状态残留。这一步特别适合排查“为什么这个物体消失了”“为什么三角形背面看不见了”这类诡异问题。第三步确认数据:把顶点数组、矩阵数据打印出来,用CPU端逻辑验证数值是否符合预期。很多情况下画面不对,根本不是GPU问题,而是CPU端算错了矩阵或者传错了指针。
举个例子,有一次我在渲染一个旋转立方体时,立方体旋转了但图形整体发生拉拽变形,看起来像是透视投影失效。我用三步定位法检查后发现,glUniformMatrix4fv()传入的矩阵数组是行主序的,但是OpenGL期望列主序,矩阵被转置了。改数据布局或者传GL_TRUE,立刻正常。这种问题如果不用数据打印定位,靠肉眼调试可以耗掉半天时间。
6. 实操心得:让老示例在新环境下跑得更顺
最后聊几个我实际操作中的个人习惯,权当收尾。对于“OpenGL-Drawing.rar”这类老工程,我现在的处理流程很固定:先解压到不含中文和空格的目录,然后用新版本Visual Studio新建空项目,把源文件拷贝进去,按上文把链接库和字符集配置好,编译通过后再一步步替换代码里的固定管线调用。替换原则是:能用现代API替换的尽量替换,不能替换的先留着,但至少把glBegin()/glEnd()这种带开始结束标记的调用,改成用VBO(顶点缓冲对象)方式提交顶点数据。
还有一个小技巧:别急着把GDI代码删掉。这类示例里的GDI代码刚好是学习“GDI和OpenGL如何共存”的最佳教材。你可以在GDI绘制部分加一行显示窗口句柄和像素格式描述信息的调试代码,这样每次运行时都能直观看到当前窗口的像素格式状态,对判断是否软渲染非常有帮助。
再分享一个很多人不知道的细节:旧版Visual C++工程里的#include <gl/gl.h>在64位系统下编译时,默认会去寻找32位还是64位的opengl32.lib,取决于你工程配置的平台。如果你在64位编译时链接不上,在链接器附加依赖项里手动指定C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Lib\x64\opengl32.lib这类路径,能绕过一些奇怪的库路径问题。当然,多数新版SDK已经统一了导入库,这个问题只在老SDK上存在。
如果你只是完成课程作业,把这个OpenGL绘图示例跑通就足够应付了;但如果你想真正掌握Windows图形编程,我建议在这个基础上做三件“课外作业”:一是把固定管线渲染改成可编程管线渲染,二是在同一窗口下用GDI叠加一个可以拖动的矩形框,三是尝试在WSL环境下用OpenGL跑通同样的渲染逻辑并对比软硬件渲染差异。这三个扩展覆盖了从API使用到性能分析的完整链路,做完之后你会对OpenGL和GDI的关系有非常透彻的理解。
本文还有配套的精品资源,点击获取