版权归作者所有,任何形式转载请联系作者。
作者:wc(来自豆瓣)
来源:https://www.douban.com/note/708074485/
围绕Linux平台上GLFW与GLEW的源码进行分析(只分析关键流程)。
Linux图形子系统简介(根据wikipedia,x.org、ajax的《dri-explanation.txt》等资料)。
我们知道操作系统上与用户交互的方式有两种:命令行与窗口系统(GUI环境)。
在Linux平台上常见的是X窗口系统(X Window System)。本质上来讲,它也是一种协议,当前的主版本号是11,所以也作X11。这种协议目前使用最普遍的实现是xorg(x.org。之前的主流是XFree86,由于XFree86于2004年修改了license,导致大量开发者和用户流向xorg,该项目最终也于2011年进入停顿状态)。这种协议除了对显示方面管理之外,还要处理输入(如鼠标、键盘。)等事物(毕竟要靠这些设备与窗口进行交互)。
X窗口系统的交互采用的是“客户端-服务器端”模型(CS,client-server)。窗口应用程序的开发都落在客户端。该系统被设计分为远端(remote)和本地端(local),远端客户端可以通过某些连接方式(如TCP网络协议)与服务端的机器交互,而本地的客户器直接就可以利用IPC(进程间通信)方式与本机的服务端交互(即客户端与服务端位于同一台机器上)。注意,客-服间的交互是建立在X这协议之上的,也就是说要靠收此发协议包来交流。
X客户端向用户提供的是一个API集合,即xorg-libX11库,其中封装了与X服务端交流的协议,而暴露出应用程序所调用的各种方法(XCB即X protocol C-language Binding,为最新实现的X11客户端库,用以替代libX11)。服务端的源码则是xorg-xserver。
X窗口系统协议还允许对其进行拓展。最有名的莫过于GLX与DRI了。
通过GLX拓展(OpenGL extension to the X Window System,X窗口系统的OpenGL拓展。最新版本为1.4),创建OpenGL context(上下文),并令OpenGL与X进行绑定(通常绘制出来的内容都是在由X系统管控下的窗口中的)。此扩展分为indirect(间接)与direct(直接)模式(完整图示可见wikipedia的glx词条)。通过indirect模式令X服务端来进行绘制工作,而利用direct模式则可配合DRI直接对图形进行(GPU硬件加速)渲染,或配合swrast走CPU软光栅,无需与X server交互,完全由client端自行负责绘制(这里所说的都是3D绘制,2D绘制靠CS模型效率不错,故不用client方自行处理)。
计算机系统之对比
long long ago,计算机系统流行的结构如左图所示的client-server模式,用户持有客户终端(显示设备+键盘等外设)与主机进行输入、输出的交互,X也遵循这一模型,绘制工作在主机处理,再反馈回终端。PC流行后,如右图,每个“终端”都有了各自的“主机”,但X系统却依然沿用之前的CS模型。只不过前者需要以其他通信方式(如TCP)与“远程”主机交互,因此称为remote模式;而后者则“主机”(服务端)与“终端”(客户端)位于同一本地端,无需其他远程通信协议辅助,因此也称为local模式,即通过IPC(进程间通信)即可实现绘制工作。(当然,有些时候前者的工作模式依然存在,这要看看计算机系统设计的具体情况)
所谓DRI,即Direct Rendering Infrastructure(直接渲染基础设施(直译)),也就是前面“ 3D硬件加速(简化步骤) ”那张图的工作流程。而DRI的主要使用者也就是mesa,基本可谓量身定制。客户端原本只能将渲染内容提交给服务端去执行,画个窗口还好,但是遇到高性能的图形渲染(比如游戏这样实时应用),这期间X协议的交互所造成的性能损失是可以预料的。利用这个拓展框架,即可将客户端的渲染需求绕过X服务端,直接通过内核传递给GPU令其加速。
GLX的间接与直接模式(实际上软光栅现已按DRI驱动形式给出,因此swrast也能以DRI方式调用,即swrast_dri.so)
这里来更加细致地探讨一下DRI框架的细节。
此处借传统mesa中的Intel 965芯片组GPU驱动系列进行讨论。
mesa编译好后,会生成名为libGL.so与i965_dri.so的链接库。后者是针对intel i965及其后续芯片组所支持GPU的用户空间“驱动”,负责将OpenGL层的绘制命令转换为对应i965系GPU的硬件指令,即前者会调用后者。(Linux链接库通常形如libxo.so,xo就是连接时用的名字,如-lxo。因此,在编译程序时使用参数-lGL即链接当前系统中能找到的libGL.so。除了这里主要提到的两个.so,根据用户在编译时的选定还有libEGL.so、swrast_dri.so等。另外,libGL.so实际连接的是libGL.so.x.y.z(x/y/z是版本号,我这里编译的是libGL.so.1.2.0),后者才是实际编译出的库,这样做法应该是为了灵活链接不同版本的OpenGL库)
前面“3D硬件加速化简的流程”图提到少画了一个关键环节,实为libdrm(userspace library for drm,针对drm的用户空间库)。利用此库即可将上层的i965_dri.so转译的GPU指令与数据进行传送(映射),主要是利用ioctl()函数与编译好的运行在内核空间的DRM进行交互。注意,从这里开始至低层,intel i965与i915系的GPU驱动全用i915来指代,不再像mesa层那样分开表示。针对i965系GPU而言,此项目编译出的libdrm.so与libdrm_intel.so库与之有关。为什么要在mesa与DRM之间加上这一层呢?此项目的介绍写的很详细了:The library provides wrapper functions for the ioctls to avoid exposing the kernel interface directly, and for chipsets with drm memory manager, support for tracking relocations and buffers. New functionality in the kernel DRM drivers typically requires a new libdrm, but a new libdrm will always work with an older kernel。即,避免直接暴露内核接口;可辅助进行内存管理;更新DRM时,借此可保证向后兼容。(该库的引用方式与libGL.so差不多,也是链接到后缀为版本号的.so上)
接下来就是DRM了。DRM即Direct Rendering Manager,直接渲染管理器,属于Linux内核的一部分。针对intel i965系的GPU而言,对应的模块文件为i915.ko。
因此,
更详细的intel i965系GPU的dri调用流程
-----------------------------我是面无表情的分割线-------------------------
如果想要看简洁且完整的示例,可参见《Tutorial: OpenGL 3.0 Context Creation(GLX)》【1】一文。
GLFW部分:
GLFW 3.2.1
一个简单的程序作为开端:
-----------------------------代码你好-------------------------
#include
#include
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
int main()
{
// glfw出屎化
if (glfwInit() == GLFW_FALSE) {
exit(EXIT_FAILURE);
};
// 选定OpenGL版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// 窗口可调整?
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
// 创建窗口
GLFWwindow* window = glfwCreateWindow(300, 200, "wc", NULL, NULL);
if (window == NULL) {
std::cout << "fuck createwindow" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
// 处理按键事件的回调函数
glfwSetKeyCallback(window, key_callback);
// 轮循忙查事件,交换前后台缓冲区
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
glfwSwapBuffers(window);
}
glfwTerminate();
return 0;
}
// 挂在glfwSetKeyCallback上的按键处理回调函数
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
std::cout << key << std::endl;
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
}
-----------------------------代码再见-------------------------
结合glfw.org上的文档进行分析。glfw按系统平台分为win32/x11/wl(Wayland)三种,先考虑x11。
glfw的核心结构是_GLFWlibrary,描述由用户设置的glfw状态。
-----------------------------头文件你好-----------------------------
#include
#include
#include
#include
-------------------------------------------------------------------------
glfwInit()是glfw的万恶之首,负责glfw自身的初始化工作(以下均去掉细枝末节(如无关紧要的X扩展),抽离关键代码。即主要体现Xlib与GLX的基本调用过程。掌握此流程再分析其他扩展也就轻而易举了)。
-----------------------------glfwInit()你好-----------------------------
// 出屎化xlib多线程,必先调用(这里分析并不开启,仅用单线程,否则分析过程会更复杂。复杂在于,
// 若开启多线程,则需在每个线程中利用TLS(thread local storage,线程本地存储)技术频繁保存和
// 读取该线程中的context上下文,从而保证每条线程工作时采用的是自己的所对应的context(如窗口信
// 息、glx信息等))
// XInitThreads();
// Linux上可以存在多个X Server,一个Server对应一个Display,Display也就是该Server管控下的硬件
// 集合(通常有键盘,鼠标与显示器)。
// 此语句令客户端与X Server服务端连接,参数为NULL即连接Linux环境变量下默认的DISPLAY
// (即连接本地的X Server,而非远端X Server等)
// 利用xdpyinfo指令可查询DISPLAY的具体信息,如当前X支持的拓展、显示模式等
// 所谓Display即描述X Server与其管下的输入、显示设备,该结构体存有相关属性
Display* display = XOpenDisplay(NULL);
// 返回XOpenDisplay获取(连接)的display上的默认显示设备screen之编号
//(一个系统中可能存在多个显示设备screen)
int screen = DefaultScreen(display);
// 获取覆于screen上的root window根窗口(即在继承层次上,无父窗口的最顶层的窗口,
// 从视觉位置上来讲是位于其子窗口的下侧)
Window root = RootWindow(display, screen);
// 创建一个唯一的上下文context(没有用到多线程,所以统统注释掉)
// XContext context = XUniqueContext();
// pthread_key_t tls_context;
// pthread_key_create(&tls_context, NULL);
-----------------------------glfwWindowHint()你好-----------------------------
// 记录用户给出的参数而已,如此这般(前两项选定OpenGL版本,后一项确定不调整窗口大小):
// glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
int major = 3;
// glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
int minor = 3;
// glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
int resizable = 0;
-----------------------------glfwCreateWindow()你好-----------------------------
// GLFW采用dlopen与dlsym来加载GLX中的函数,
// 更简单如【1】可直接通过-lGL -lX11语句链接GLX与X11lib库
// libGL.so包括mesa3d中GLX与OpenGL函数的实现(当然,实际上它还要引用其他.so)
void* glx_handle = dlopen("libGL.so.1", RTLD_LAZY | RTLD_GLOBAL);
/***************************************************************/
// 这里我们直接利用dlsym来加载后续要用到的GLX函数
// 这些GLX函数的定义可见《GLX 1.4 Specification》
// 定义函数的指针形如PFNXOXOPROC,PFN即Pointer to the FunctioN(指向函数XOXO的指针)
// 此指向的是一个 PROCedure (函数)【语见《Load OpenGL Functions》】
// 下面这一坨可以忽略,知道在做什么即可……
/////////////////// GLXFBConfig *glXGetFBConfigs(Display *dpy, int screen, int *nelements); ///////////////////
typedef struct __GLXFBConfig* GLXFBConfig;
typedef GLXFBConfig* (*PFNGLXGETFBCONFIGSPROC)(Display*,int,int*);
PFNGLXGETFBCONFIGSPROC GetFBConfigs;
#define glXGetFBConfigs GetFBConfigs
GetFBConfigs = (PFNGLXGETFBCONFIGSPROC)dlsym(glx_handle, "glXGetFBConfigs");
//////////// int glXGetFBConfigAttrib(Display *dpy, GLXFBConfig config, int attribute, int *value); /////////////
typedef int (*PFNGLXGETFBCONFIGATTRIBPROC)(Display*,GLXFBConfig,int,int*);
PFNGLXGETFBCONFIGATTRIBPROC GetFBConfigAttrib;
#define glXGetFBConfigAttrib GetFBConfigAttrib
GetFBConfigAttrib =
(PFNGLXGETFBCONFIGATTRIBPROC)dlsym(glx_handle, "glXGetFBConfigAttrib");
/////////////////// XVisualInfo *glXGetVisualFromFBConfig(Display *dpy, GLXFBConfig config); ////////////
typedef XVisualInfo* (*PFNGLXGETVISUALFROMFBCONFIGPROC)(Display*,GLXFBConfig);
PFNGLXGETVISUALFROMFBCONFIGPROC GetVisualFromFBConfig;
#define glXGetVisualFromFBConfig GetVisualFromFBConfig
GetVisualFromFBConfig =
(PFNGLXGETVISUALFROMFBCONFIGPROC)dlsym(glx_handle, "glXGetVisualFromFBConfig");
//////////////////// void *glXGetProcAddressARB(const GLubyte *procName) ////////////////////
typedef void (*__GLXextproc)(void);
typedef unsigned char GLubyte;
typedef __GLXextproc (* PFNGLXGETPROCADDRESSPROC)(const GLubyte *procName);
PFNGLXGETPROCADDRESSPROC GetProcAddressARB;
#define glXGetProcAddressARB GetProcAddressARB
GetProcAddressARB =
(PFNGLXGETPROCADDRESSPROC)dlsym(glx_handle, "glXGetProcAddressARB");
/////////////////// GLXContext glXCreateContextAttribsARB( Display *dpy, GLXFBConfig config, /////////////////// GLXContext share_context, Bool direct, const int *attrib_list);
typedef struct __GLXcontext* GLXContext;
typedef GLXContext (*PFNGLXCREATECONTEXTATTRIBSARBPROC)
(Display*,GLXFBConfig,GLXContext,Bool,const int*);
PFNGLXCREATECONTEXTATTRIBSARBPROC CreateContextAttribsARB;
#define glXCreateContextAttribsARB CreateContextAttribsARB
CreateContextAttribsARB =
(PFNGLXCREATECONTEXTATTRIBSARBPROC)glXGetProcAddressARB(
(const GLubyte*)"glXCreateContextAttribsARB");
// GLXWindow glXCreateWindow(Display *dpy, GLXFBConfig config, Window win, const int *attrib list);
typedef XID GLXWindow;
typedef GLXWindow (*PFNGLXCREATEWINDOWPROC)(Display*,GLXFBConfig,Window,const int*);
PFNGLXCREATEWINDOWPROC CreateWindow;
#define glXCreateWindow CreateWindow
CreateWindow = (PFNGLXCREATEWINDOWPROC)dlsym(glx_handle, "glXCreateWindow");
///////////////// Bool glXMakeCurrent(Display *dpy, GLXDrawable draw, GLXContext ctx); /////////////////
typedef XID GLXDrawable;
typedef Bool (*PFNGLXMAKECURRENTPROC)(Display*,GLXDrawable,GLXContext);
PFNGLXMAKECURRENTPROC MakeCurrent;
#define glXMakeCurrent MakeCurrent
MakeCurrent = (PFNGLXMAKECURRENTPROC)dlsym(glx_handle, "glXMakeCurrent");
///////////////// void *glXGetProcAddress(const GLubyte *procName); ////////////////
PFNGLXGETPROCADDRESSPROC GetProcAddress;
#define glXGetProcAddress GetProcAddress
GetProcAddress = (PFNGLXGETPROCADDRESSPROC)dlsym(glx_handle, "glXGetProcAddress");
///////////////// int glXSwapIntervalMESA(unsigned int interval) ///////////////////
typedef int (*PFNGLXSWAPINTERVALMESAPROC)(int);
PFNGLXSWAPINTERVALMESAPROC SwapIntervalMESA;
#define glXSwapIntervalMESA SwapIntervalMESA
SwapIntervalMESA = (PFNGLXSWAPINTERVALMESAPROC)glXGetProcAddress(
(const GLubyte*)"glXSwapIntervalMESA");
///////////////// void glXSwapBuffers(Display *dpy, GLXDrawable draw); ////////////////////
typedef void (*PFNGLXSWAPBUFFERSPROC)(Display*,GLXDrawable);
PFNGLXSWAPBUFFERSPROC SwapBuffers;
#define glXSwapBuffers SwapBuffers
SwapBuffers = (PFNGLXSWAPBUFFERSPROC)dlsym(glx_handle, "glXSwapBuffers");
//////////////////// glXDestroyWindow(Display *dpy, GLXWindow win); ////////////////
typedef void (*PFNGLXDESTROYWINDOWPROC)(Display*,GLXWindow);
PFNGLXDESTROYWINDOWPROC DestroyWindow;
#define glXDestroyWindow DestroyWindow
DestroyWindow = (PFNGLXDESTROYWINDOWPROC)dlsym(glx_handle, "glXDestroyWindow");
///////////////////// void glXDestroyContext(Display *dpy, GLXContext ctx); ////////////////////
typedef void (*PFNGLXDESTROYCONTEXTPROC)(Display*,GLXContext);
PFNGLXDESTROYCONTEXTPROC DestroyContext;
#define glXDestroyContext DestroyContext
DestroyContext = (PFNGLXDESTROYCONTEXTPROC)dlsym(glx_handle, "glXDestroyContext");
/***************************************************************/
// 根据指定的screen遍历出系统中GLXFBConfig的各种属性组合(也就是帧缓冲区各种属性的组合搭
// 配,如rgba等各通道的深度值),返回值是属性搭配的实际组合,nativeCount是属性组合的数量,
// 可在终端利用glxinfo指令查看(GLXFBConfig->GLX frame buffer(帧缓冲区) configuration)
int nativeCount;
GLXFBConfig *nativeConfigs = glXGetFBConfigs(display, screen, &nativeCount);
int value;
struct fbconfig {
int redBits;
int greenBits;
int blueBits;
int alphaBits;
int depthBits;
int stencilBits;
bool doublebuffer;
} fbc;
// 这里只关心rgba、深度缓冲区、模板缓冲区的深度值以及是否双缓冲
fbc = {
8, // redBits;
8, // greenBits;
8, // blueBits;
8, // alphaBits;
24, // depthBits;
8, // stencilBits;
1, // doublebuffer;
};
/////////////////////// 只列出与本程序有关的属性,其他详见《GLX 1.4 Specification》 ////////////////////////
#define GLX_DOUBLEBUFFER 5
#define GLX_RED_SIZE 8
#define GLX_GREEN_SIZE 9
#define GLX_BLUE_SIZE 10
#define GLX_ALPHA_SIZE 11
#define GLX_DEPTH_SIZE 12
#define GLX_STENCIL_SIZE 13
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 遍历属性,选出第一个符合我们条件的属性组合。实际上,可能会有多个属性组合满足我们的条件,
// 在大多数的OpenGL辅助工具中,会基于预先选定的基本条件,再配合其他属性(如支持的采样点
// 数)选出最佳的属性组合。这里删繁就简,选中第一个满足我们上述属性条件的组合即可
GLXFBConfig result;
for (int i = 0; i < nativeCount; i++) {
// r通道是否8位?不满足就请下一位
glXGetFBConfigAttrib(display, nativeConfigs[i], GLX_RED_SIZE, &value);
if (value != fbc.redBits)
continue;
// g通道是否8位?不满足就请下一位
glXGetFBConfigAttrib(display, nativeConfigs[i], GLX_GREEN_SIZE, &value);
if (value != fbc.greenBits)
continue;
// b通道是否8位?不满足就请下一位
glXGetFBConfigAttrib(display, nativeConfigs[i], GLX_BLUE_SIZE, &value);
if (value != fbc.blueBits)
continue;
// alpha通道是否8位?不满足就请下一位
glXGetFBConfigAttrib(display, nativeConfigs[i], GLX_ALPHA_SIZE, &value);
if (value != fbc.alphaBits)
continue;
// 深度通道是否24位?不满足就请下一位
glXGetFBConfigAttrib(display, nativeConfigs[i], GLX_DEPTH_SIZE, &value);
if (value != fbc.depthBits)
continue;
// 模板通道是否8位?不满足就请下一位
glXGetFBConfigAttrib(display, nativeConfigs[i], GLX_STENCIL_SIZE, &value);
if (value != fbc.stencilBits)
continue;
// 是否支持双缓冲区?不满足就请下一位
glXGetFBConfigAttrib(display, nativeConfigs[i], GLX_DOUBLEBUFFER, &value);
if (value != fbc.doublebuffer)
continue;
// 满足所有条件就记录下(result),并退出查找
result = nativeConfigs[i];
break;
}
XFree(nativeConfigs);
// X下的每个Windows对象都要靠一个Visual结构体创建,XVisualInfo由{Visual, Screen, Depth}三部
// 分组成供GLX使用,而XVisualInfo可由以GLXFBConfig结构体为参数的函数创建
XVisualInfo *vi = glXGetVisualFromFBConfig(display, result);
Visual *visual = vi->visual;
int depth = vi->depth;
// 窗口属性,边框像素值、关联于窗口的色彩表、并监测窗口事件
const unsigned long wamask = CWBorderPixel | CWColormap | CWEventMask;
// 此颜色表Colormap中存有硬件支持的rgb组合,屏幕显示像素时将以索引从中获取具体颜色值
// 即借此Colormap将程序中计算出的像素颜色值与实际硬件支持的颜色值之间进行映射
Colormap colormap = XCreateColormap(display,
root,
visual,
AllocNone);
XSetWindowAttributes wa;
wa.colormap = colormap;
wa.border_pixel = 0;
// 监控的事件,KeyPressMask按键事件,这里用于监视用户按下Esc键
// VisibilityChangeMask则监控窗口可视状态的改变,关掉窗口即不可视
// 可以尝试去掉VisibilityChangeMask,再看看窗口还关得掉否?呵呵……
wa.event_mask = KeyPressMask | VisibilityChangeMask;
// 根据上述配置属性创建窗口
Window handle = XCreateWindow(display,
root,
0, 0,
300, // width,
200, // height,
0,
depth,
InputOutput,
visual,
wamask,
&wa);
// 窗口管理器与客户端交流的协议(详见《Client to Window Manager Communication》,ICCCM)
// 监控窗口事件(WM_XOXO,WM=>window manager(窗口管理器)),WM_PROTOCOLS包括
// 一系列窗口动作,如WM_DELETE_WINDOW关闭窗口(我们要借此实现点击关闭按钮来关闭窗口)
Atom WM_PROTOCOLS =
XInternAtom(display, "WM_PROTOCOLS", False);
Atom WM_DELETE_WINDOW =
XInternAtom(display, "WM_DELETE_WINDOW", False);
Atom protocols[] =
{
WM_DELETE_WINDOW
};
XSetWMProtocols(display, handle,
protocols, sizeof(protocols) / sizeof(Atom));
// 根据此前将GLFW_RESIZABLE设置为GL_FALSE(禁止窗口缩放)进行配置
// 将窗口最大、最小宽度均设为300像素;将窗口最大、最小高度均为200像素(也就是不能调整窗口)
XSizeHints* hints = XAllocSizeHints();
hints->flags |= (PMinSize | PMaxSize);
hints->min_width = hints->max_width = 300;
hints->min_height = hints->max_height = 200;
// 配置Windows Gravity,即当前窗口如何随父窗口尺寸的调整而重新定位
hints->flags |= PWinGravity;
hints->win_gravity = StaticGravity;
// 对以上窗口大小等描述进行设置
XSetWMNormalHints(display, handle, hints);
XFree(hints);
// 设置窗口标题
Xutf8SetWMProperties(display,
handle,
"wc", NULL,
NULL, 0,
NULL, NULL, NULL);
XFlush(display);
// 详见glxext.h
#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
// 也就是用户在glfwWindowHint()中定义的major与minor
// OpenGL的版本
int attribs[] {
GLX_CONTEXT_MAJOR_VERSION_ARB, 3, // major
GLX_CONTEXT_MINOR_VERSION_ARB, 3, // minor
None, None
};
// 创建GLX(OpenGL渲染)上下文context
GLXContext glx_context = glXCreateContextAttribsARB(
display,
result,
0,
true,
attribs);
// 针对此前创建的X窗口,创建对应的“在屏(onscreen,与离屏off-screen相反)”渲染区域
// 后续的OpenGL操作将绘制到此区域(也就是对应的X窗口里)
GLXWindow glx_window =
glXCreateWindow(display, result, handle, NULL);
XFlush(display);
// 将此窗口映射至屏幕,这样,该窗口Window 才可显示出来
int x = XMapWindow(display, handle);
XEvent dummy;
// XCheckTypedWindowEvent从事件队列中查找第一个我们指定的事件(这里是VisibilityNotify,
// 即窗口的可见显示事件),意即利用while循环等待窗口的显示事件
while (!XCheckTypedWindowEvent(display,
handle,
VisibilityNotify,
&dummy)) {
}
// 窗口显示在最前端
XRaiseWindow(display, handle);
// 窗口获取焦点
XSetInputFocus(display, handle,
RevertToParent, CurrentTime);
XFlush(display);
-----------------------------glfwMakeContextCurrent()你好-----------------------------
// 将glx_context设置为当前线程的GLX rendering context(渲染上下文)
// 将GLX上下文(此处的glx_context)与窗口(glx_window)相绑定
glXMakeCurrent(display,
glx_window,
glx_context);
-----------------------------glfwSwapInterval()你好-----------------------------
// 开启垂直同步。此函数指定交换缓冲区所等待的最小时间间隔,单位为一个视频帧绘制周期。
// 设置为1表示等待每一帧显示完整(也就是等一个视频帧绘制周期后,再交换缓冲区)
// 此功能不属于GLX协议,而是一种拓展。此拓展有三种实现,即glXSwapIntervalSGI()、
// glXSwapIntervalMESA()与SwapIntervalEXT(),视具体系统而定,可由glXQueryExtensionsString()来
// 查询当前系统支持哪一种实现
glXSwapIntervalMESA(1);
-----------------------------while (!glfwWindowShouldClose())你好-----------------------------
////////////////////// main()函数之前定义的全局变量与函数 //////////////////////
typedef void (* keyfun)(int);
keyfun key;
int stop = 0;
void key_callback(int keycode)
{
// ESC在X中的键码为9,借此修改while循环中的控制标志,停止窗口消息循环的处理
// 这便是glfwWindowShouldClose()背后的基本原理
if (keycode == 9)
stop = 1;
}
//////////////////////////////////////////////////////////////////////////////
// glfwSetKeyCallback(key_callback),即挂key_callback的原理
// 为防没有对key赋函数(也就是用户没有定义按键回调函数),先将其定义为NULL
key = NULL;
key = key_callback; // 挂载按键回调callback函数
stop = 0;
// 其实就是处理窗口消息队列中消息的消息循环
while (!stop) {
int count = XPending(display); // 返回事件队列中未移除的事件数量
int keycode;
while (count--) {
XEvent event;
keycode = 0;
XNextEvent(display, &event); // 从事件队列中获取一个事件,并将其从中移除
if (event.type == KeyPress) // 是否按键?
keycode = event.xkey.keycode; // 获取键码
switch (event.type) {
case KeyPress: // 如果有键被按下,且设置了按键回调函数,则以键码为参数执行回调函数
if (key != NULL)
key(keycode);
break;
case ClientMessage:
{
const Atom protocol = event.xclient.data.l[0];
// 处理WM_PROTOCOLS下的WM_DELETE_WINDOW消息(删除窗口)
// 这里为设置stop旗标,退出事件处理循环,并销毁此前创建的各种资源
if (event.xclient.message_type == WM_PROTOCOLS) {
if (protocol == WM_DELETE_WINDOW)
{
stop = 1;
}
}
break;
}
default:
break;
} // switch
} // while(count)
XFlush(display);
// glfwSwapBuffers(),交换前后台缓冲区
glXSwapBuffers(display, glx_window);
} // while(stop)
-----------------------------glfwTerminate()你好-----------------------------
// 释放、销毁此前创建的各种资源,注意顺序……
glXMakeCurrent(display, None, NULL);
glXDestroyWindow(display, glx_window);
glXDestroyContext(display, glx_context);
XUnmapWindow(display, handle);
XDestroyWindow(display, handle);
XFreeColormap(display, colormap);
XFlush(display);
XCloseDisplay(display);
dlclose(glx_handle);
-----------------------------我是面无表情的分割线-------------------------
GLEW部分:
GLEW 2.1.0
Linux编译过程通常要从makefile等脚本开始分析。不过大多数时候我都不愿意读这玩意儿,挨个儿查变量的滋味并不好受……
分析之前先看编译文档《Automatic Code Generation》,khronos.org的《Load OpenGL Functions》了解代码的基本执行过程。
可知,分析glewInit()即可。在此之前先聊聊加载OpenGL函数有关的内容(根据OpenGL wiki与specs整理)。
自OpenGL 3.0就开始酝酿deprecation model(废止模型),标记了未来版本会从Core去掉的特性(此前每次OpenGL协议更新基本都在增加特性,但这次开始决定去掉包袱,从核心特性中删减一些内容)。但3.0并没有实际去删减函数,而是利用设置glXCreateContextAttribsARB函数GLX_CONTEXT_FLAGS_ARB属性的GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB标志(在glfw中是配置glfwWindowHint函数的GLFW_OPENGL_FORWARD_COMPAT标志),将OpenGL context分为两种: forward compatible context(向前兼容上下文)与full context(完整上下文),顾名思义,前者是去掉过时特性的OpenGL上下文;而后者还是保持与之前版本的兼容性,具有完整的特性,根据specs中可以看出这一点。
OpenGL 3.1版发生大变动,API做了巨大调整,去掉了此前3.0版中标记的函数,对之前版本的兼容性自然不是很好。因此在OpenGL 3.1中,增加了GL_ARB_compatibility拓展,借此即可使context保留所有函数(包括过时或删减的函数在内)的入口点和各种OpenGL的枚举项等(也就是通过这种方式仍可加载所有的OpenGL方法)。从这里来说,OpenGL 3.1是一个分水岭。
及至OpenGL 3.2,又正式引入了profile机制(profile有轮廓的意思,可以想象此机制“描绘”了OpenGL函数集的实现“轮廓”,也就是定义了函数子集的范围(从这种角度来说,OpenGL ES是否也是一种OpenGL profile?))。为什么说正式?因为在OpenGL 3.0时文档已指出了profile这个概念,但是在OpenGL 3.2时才真正划分为两种profile,OpenGL 3.0、3.1中仅有一种profile)。即,从此版本后,OpenGL每版的协议分为“Core Profile”与“Compatibility Profile”两种,即本版的核心函数集,以及初代至今的全部函数集。而OpenGL的实现则必须(must)能创建出对应的“Core context”,而或许(may,可见后者的实现不是必备的)能创建“Compatibility context”。加入此机制的原因也很简单,即取代GL_ARB_compatibility拓展(从specs也能看出,此拓展只出现在OpenGL 3.1)。要设置此项,可调整glXCreateContextAttribsARB的GLX_CONTEXT_PROFILE_MASK_ARB属性(在glfw中是借助glfwWindowHint函数调整GLFW_OPENGL_PROFILE属性)。这又是一个分水岭,但是大局已定,后续的OpenGL上下文均延续此方法。
众所周知,要利用OpenGL就需要其头文件、函数库以及GPU硬件驱动。驱动通常由硬件厂家与操作系统开发商配合开发。而OpenGL的函数库实现则是五花八门:有硬件公司自己做的闭源库(通常是随闭源驱动一块安装),操作系统自带的OpenGL库(如Windows),以及开源库(如mesa3d)等。
OpenGL头文件的变迁也比较复杂,初期版本的OpenGL把函数定义与宏等内容都放在
比如,在Windows平台上,自带OpenGL实现并没有按OpenGL协议更新,而是仅维持在OpenGL 1.1版本上(意图很明显)。对于跨平台这件事儿上就出了问题。Windows的
这样来说,对于Linux而言,理论上,如果安装了mesa的话,其实所有的OpenGL函数基本上都是直接暴露给用户的。只要加上
glew有自己的
-----------------------------我是面无表情的分割线-------------------------
下面正式分析glewInit()的流程。
根据glew的名称(The OpenGL Extension Wrangler Library)可知,其关键功能在于两部分,一是加载profile,二是加载扩展。(从某种角度来说,每代profile中添加的函数都是前一代的拓展,因此可将profile中的函数看作拓展函数)另外,glewInit()也负责加载glx拓展函数。
其工作原理也很简单,加载当前系统中存在的所有扩展,并利用数组记录下来。待用户使用时,先查找数组记录,确定扩展存在后方可调用。
首先要确保glewInit()函数在glXMakeCurrent()函数之后调用(即glfw中的glfwMakeContextCurrent),也就是在执行glewInit()前,需要将此前以GLX创建的OpenGL context设置为当前线程中的GLX渲染上下文,并将其与目标窗口绑定,这样才能实现与对应低层驱动的关联,从而拿到系统中相应OpenGL的context上下文。
const GLubyte *s = glGetString(GL_VERSION);
借助s解析出所采用的OpenGL版本,major,minor。假设我们采用的是OpenGL 3.2,则major = 3且minor = 2。
用bool类型的__GLEW_VERSION_4_6、__GLEW_VERSION_4_5等作标志,根据上述查到的版本号标记出采用的OpenGL功能集,如major = 3且minor = 2,则将__GLEW_VERSION_3_2至__GLEW_VERSION_1_1都标记为true。
glew中存在三种数组,用来描述当前平台对拓展的支持情况,分别为:_glewExtensionString[]、_glewExtensionLookup[]与_glewExtensionEnabled[],三种数组大小相同,其中相同的索引指向的是相同的拓展项。功能如下:
初始过程结束后,将根据getStringi(OpenGL >= 3)查询到的系统支持的拓展,对_glewExtensionString[]中的对应项填写为true,表示当前系统支持对应拓展,此数组用于实现glewGetExtension()函数。
static GLboolean _glewExtensionString[801];
glewExtensionLookup[]中存有按字母顺序排列的全体拓展名,便于与给出的待查询的拓展名进行比对查找,从而得到对应的扩展索引:
static const char * _glewExtensionLookup[801] = {
...
#ifdef GL_AMD_conservative_depth
"GL_AMD_conservative_depth",
#endif
...
};
_glewExtensionEnabled[]中的各项表示当前的系统是否支持此扩展(即是否能从驱动中加载对应扩展函数的入口点,供用户调用),如果在glewInit()函数前加入
glewExperimental = GL_TRUE;
语句,则glew会在初始化期间试图加载OpenGL的所有扩展函数。此数组与_glewExtensionString[]的区别在于,在开启glewExperimental后,_glewExtensionEnabled[]将遍历加载系统中支持的所有拓展,而不仅限于getStringi查到的结果(有些拓展getStringi查不到)。此数组用于实现 glewIsSupported()函数
static GLboolean* _glewExtensionEnabled[801] = {
...
#ifdef GL_AMD_conservative_depth
&__GLEW_AMD_conservative_depth,
#endif
....
};
首先,将bool类型的_glewExtensionString[801]数组与_glewExtensionEnabled[]数组初始为false,等待后续根据系统支持的拓展对其bool项进行改写为true。
如果OpenGL版本 >= 3,利用getIntegerv(GL_NUM_EXTENSIONS, &n);获取当前系统中的拓展数量。再依次根据系统平台支持的拓展,填写_glewExtensionString[]与_glewExtensionLookup[]。
// 用伪代码与注释简单概述流程,没有列全部代码
getIntegerv(GL_NUM_EXTENSIONS, &n);
for (int i = 0; i < n; i++) {
// 获取拓展名
const char *ext = (const char *) getStringi(GL_EXTENSIONS, i);
// 将ext与_glewExtensionLookup[]中的字符串进行比对,这样就可确定此拓展在
// _glewExtensionString[]与_glewExtensionEnabled[]中的数组索引值
// 并将_glewExtensionString[]与_glewExtensionEnabled[]中的对应项置为true,说明支持此拓展
// _glewExtensionString[ext_index] = true;
idx = find_ext_idx(ext); // find_ext_idx(ext) => if(!strcmp(_glewExtensionLookup[i], ext)) return i;
_glewExtensionString[idx] = true;
_glewExtensionEnabled[idx] = true;
}
定义好加载函数集合之后,照理就可以开始加载函数了。但是考虑到有些处于实验阶段中的驱动(experimental driver)以及预览版驱动(pre-release driver)里的扩展,仅按照标准的gl机制可能查询不到,所以定义了glewExperimental标志,将此标志设置为true即可利用glXGetProcAddressARB函数将特定的专有拓展或实验性质的拓展试着加载一遍。因此,当设glewExperimental=GL_TRUE;后,会将系统中所有能加载成功扩展(包括getStringi()不能查找到的)的_glewExtensionEnabled[]对应项设为true,表明“环境中能成功加载的所有拓展函数,我已经依次加载,并都列在_glewExtensionEnabled[]中了”。
简而言之,如果加入glewExperimental=GL_TRUE;语句,则_glewExtensionEnabled[]中的拓展更全面,即glewIsSupported()函数的查找结果完整。而_glewExtensionString[]中标记的仅是由glGetStringi()能找到的拓展。若不加上述语句,则_glewExtensionEnabled[]与_glewExtensionString[]中的拓展基本一致,也就是glewIsSupported()与glewGetExtension()的查询结果基本一致。另,glew文档提到:glew 1.3.0 release版开始 glewGetExtension()已被glewIsSupported()替代。
随后就开始正式加载扩展函数。
首先,根据定义的core profile加载函数,上面我们假设加载OpenGL 3.2,根据前面的执行流程可知,此时glew将__GLEW_VERSION_1_1至__GLEW_VERSION_3_2依次设置为true,则:
// 伪代码,
// 出来,所以glew.c中从OpenGL 1.2函数开始加载
if (glewExperimental || __GLEW_VERSION_1_2)
// 加载profile 1.2中的任一函数失败则表示不支持此版OpenGL
if (glXGetProcAddressARB(OpenGL_1_2_functions))
_glewExtensionEnabled[1_2_idx] = true;
else
_glewExtensionEnabled[1_2_idx] = false;
if (glewExperimental || __GLEW_VERSION_1_3)
// 加载profile 1.3中的任一函数失败则表示不支持此版OpenGL
if (glXGetProcAddressARB(OpenGL_1_3_functions) )
_glewExtensionEnabled[1_3_idx] = true;
else
_glewExtensionEnabled[1_3_idx] = false;
......
if (glewExperimental || __GLEW_VERSION_3_2)
// 加载profile 3.2中的任一函数失败则表示不支持此版OpenGL
if (glXGetProcAddressARB(OpenGL_3_2_functions))
_glewExtensionEnabled[3_2_idx] = true;
else
_glewExtensionEnabled[3_2_idx] = false;
……
// 根据glewExperimental或用户定义的__GLEW_VERSION_x_o,最多依次加载到最新版OpenGL 4.6
if (glewExperimental || __GLEW_VERSION_4_6)
// 加载profile中的任一函数失败则表示不支持此版OpenGL
if (glXGetProcAddressARB(OpenGL_4_6_functions))
_glewExtensionEnabled[4_6_idx] = true;
else
_glewExtensionEnabled[4_6_idx] = false;
接下来加载专有拓展或实验拓展等:
if (glewExperimental || __GLEW_3DFX_tbuffer)
// 加载GL_3DFX_tbuffer中的任一函数失败则表示不支持此扩展
if (glXGetProcAddressARB(3DFX_tbuffer_functions))
_glewExtensionEnabled[3DFX_tbuffer_idx] = true;
else
_glewExtensionEnabled[3DFX_tbuffer_idx] = false;
if (glewExperimental || __GLEW_AMD_debug_output)
// 加载GL_AMD_debug_output中的任一函数失败则表示不支持此扩展
if(glXGetProcAddressARB(AMD_debug_output_functions))
_glewExtensionEnabled[AMD_debug_output_idx] = true;
else
_glewExtensionEnabled[AMD_debug_output_idx] = false;
……
可见,如果将glewExperimental设置为true,则遍历系统中存在的一切拓展。
加载完这一系列拓展后,OpenGL拓展部分也就结束了。用户可先利用glewIsSupported()与glewGetExtension()查找待执行的拓展函数在当前系统中是否存在,随后再进行调用。亦可采用if (GLEW_{拓展名,如ARB_vertex_program})或
if (GLEW_VERSION_{core版本,如3_2})来检测OpenGL扩展函数与核心函数是否存在(GLEX_XOXO其实就是_glewExtensionEnabled[]中bool项的封装)。
接下来是加载glx函数部分。
// 由于glXGetCurrentDisplay函数在glx 1.2版中加入,因此借助它来初始化glx 1.2的核心函数
glewGetProcAddress((const GLubyte*)"glXGetCurrentDisplay");
// 检测当前环境下的X display是否可正常使用
display = glXGetCurrentDisplay();
if (display == NULL)
blablabla……
// 查询display支持的glx版本
glXQueryVersion(display, &major, &minor);
// 如果minor = 4, 则将__GLXEW_VERSION_1_4至__GLXEW_VERSION_1_0设置为true
// 如果minor = 3, 则将__GLXEW_VERSION_1_3至__GLXEW_VERSION_1_0设置为true
// 基本现在系统中支持的glx版本均为1.4,所以这里假设采用minor = 4这一选项
// 借助glXGetClientString返回系统支持的glx扩展名
glx_ext = (const GLubyte*)glXGetClientString(display, GLX_EXTENSIONS);
返回的扩展字符串glx_ext形如"GLX_ARB_context_flush_control GLX_ARB_create_context GLX_ARB_create_context_profile……"(即“扩展0 扩展1 扩展2 ……”的形式)借助这一形式即可解析出其中的扩展名,用于比对、确定是否可加载某一拓展。
前面已经提到,利用glXGetCurrentDisplay加载了glx 1.2。所以再来尝试加载glx 1.3。
if (glewExperimental || __GLXEW_VERSION_1_3)
// 加载GLX_VERSION_1_3中的任一函数失败则表示不支持此扩展
if (glXGetProcAddressARB(GLX_VERSION_1_3_functions))
__GLXEW_VERSION_1_3 = true;
else
__GLXEW_VERSION_1_3 = false;
后面就是加载glx有关的零七八碎的专有扩展或实验拓展等,如:
// 伪代码,从之前获得的系统支持的拓展名中,查找是否支持GLX_AMD_gpu_association
__GLXEW_AMD_gpu_association = search_ext("GLX_AMD_gpu_association", glx_ext);
if (glewExperimental || __GLXEW_AMD_gpu_association)
if (glXGetProcAddressARB(GLX_AMD_gpu_association_functions))
__GLXEW_AMD_gpu_association = true;
else
__GLXEW_AMD_gpu_association = false;
……
完成这部分处理后,用户可先执行glxewIsSupported()检测当前可使用的glx扩展,随即调用对应函数。亦可用if (GLXEW_{glx扩展名,如AMD_gpu_association})来进行检测(GLXEW_XOXO其实就是glx拓展bool项(如__GLXEW_AMD_gpu_association)的封装)。
至此,glew库完成其加载OpenGL与GLX拓展的任务。