X Window 的 OpenGL 扩展 —— GLX

LiYanrui posted @ Feb 08, 2009 05:22:17 AM in 数据可视化 with tags OpenGL X Window Xlib GLX , 26993 阅读

GLX 与显卡驱动

为了取得广泛的平台支持,OpenGL 是不依赖任何窗口系统的,但是我们使用 OpenGL 绘制的三维场景却需要嵌入在某种窗口程序中才可以为人所见。似乎也是约好了的,目前主流的窗口系统(X Window, MS Windows, Mac OS)只是提供了完善的二维图形交互环境,而将复杂的三维图形渲染任务交给 OpenGL (MS Windows 更热衷于用 Direct3D 来取代 OpenGL)。当 OpenGL 与窗口系统们达成了相互依存的默契之后,便出现了用于衔接 OpenGL 与窗口系统的扩展库,这些扩展库的主要目标就是实现 OpenGL 图形与对应窗口画面之间的转换。GLX 库解决了 X Window 中的 OpenGL 图形渲染问题。同样的,WGL 和 AGL 库分别解决了 MS Windows 和 Mac OS 窗口系统中的 OpenGL 图形渲染问题。

GLX 像胶水一样,建立了 X Window、OpenGL 以及 3D 硬件加速驱动之间的联系。在 Linux 中,如果未有安装相应的支持 3D 加速的显卡驱动,那么 GLX 可以调用 Mesa (Unix-like 系统中应用最为广泛的 OpenGL 实现) 的 3D 软件加速来取得 X Window 与 OpenGL 的集成;当然,这种做法所实现的 3D 渲染效率就要大打折扣,并且也会累的 CPU 气喘。多了解一下 X Window 是如何驱动硬件 3D 加速,可以更加深刻的理解 GLX 的工作机理。不过,目前这方面的文档实在太少了,下面文字是我个人的一些猜测,未必正确。

目前,Nvidia 和 ATI 这两大主流显卡均面向 Linux 提供了驱动程序,但是它们的做法不相同。Nvidia 驱动提供了自己的 GLX 库,在安装驱动时,会替换掉 X Window 的标准 GLX 库,另外也会将 Mesa 提供的并且只具备 3D 软件加速支持的 libGL 库替换为自己的 libGL 库。Nvidia 驱动还会提供一个 Linux 内核模块,这样可以让 X Window 通过内核访问显卡的帧缓冲区。与 Nvidia 驱动相比, ATI 则显得比较规范(此处不论其驱动的质量优劣),它利用 DRI + Mesa 来实现 3D 硬件加速,而不是像 Nvidia 驱动那样,什么都是自己去实现。最近,ATI 的开源 3D 加速驱动也渐渐浮出水面,以后买显卡,得多考证考证 ATI 了。

直接渲染与间接渲染

GLX 所支持的 OpenGL 三维图形渲染分为直接渲染与间接渲染,其区别如下图所示:

这里,勿将直接渲染与“硬件加速”的概念混淆了。无论是直接渲染还是间接渲染,都可以利用硬件加速,当然前提是显卡驱动要提供硬件加速支持。同样,也不要将间接渲染与“软件加速”的概念混淆。直接渲染,可以简单理解为本地渲染,即在同一台机器上的 X Client/Server 环境中实现三维图形渲染;相应地,间接渲染可以理解为远程渲染,也就是在网络环境中实现三维图形硬件加速渲染。

GLX 的版本

目前,应该有 5 个版本,从 1.0 到 1.4,其中比较应用比较广泛的是版本是 1.2,GtkGLExt 所使用的 GLX 库就是这一版本(据说是为了获得跨平台支持不得己而为之)。Nvidia 180.27 提供的 GLX 版本是 1.4,至于 X Window 默认的 GLX 版本,我不清楚。GLX 是向后兼容的,因此,目前许多 X Window 程序使用 GLX 1.2 也是可以正常运行。GLX 1.3 是变动幅度比较大的版本,但是介绍它如何在编程中使用的文档很稀有。下面对一个 GLX 1.2 的编程示例略作修改,权当 GLX 1.3 + Xlib 的编程实例,而至于该实现是否正确,不敢保证。

GLX 编程示例

Step 1: 包含基本的头文件

#include<stdio.h>
#include<X11/Xlib.h>
#include<GL/glx.h>
#include<GL/glu.h>

stdio.h 是需要的,因为程序中要在终端里 printf 一些有价值的错误检测信息。Xlib.h 和 glx.h 都是必须的,其中由于 glx.h 中包含了 gl.h,所以不必再像普通的 OpenGL 程序那样,再去包含一次 gl.h 了。glu.h 之所以需要,是因为程序中使用了 GLU 库的 gluLookAt 函数来设置场景观察方位,另外就是用了 GLU 库提供的二次曲面(本例中是球面)绘制函数。

Step 2: main 函数与好多的变量

int
main (int argc, char *argv[])
{
        /* 建立 X 窗口所需的变量 */
        Display *dpy;
        Window root;
        XVisualInfo *vi;
        Colormap cmap;
        XSetWindowAttributes swa;
        Window win;
        XWindowAttributes gwa;
        XEvent xev;
        unsigned int width = 300;
        unsigned int height = 300;

        /* 在 X 窗口中渲染 OpenGL 图形所需变量 */
        GLXFBConfig *fc;
        GLXWindow glw;
        GLXContext glc;
        int att[] = {GLX_RENDER_TYPE, GLX_RGBA_BIT,
                          GLX_DOUBLEBUFFER, True,
                          GLX_DEPTH_SIZE, 16,
                          None};
        int nelements;

上面代码中的这些变量,大部分都是建立一个 X 窗口时必须的,要理解它们,可以阅读 xlib 手册。事实上,对于桌面应用程序开发,直接使用 xlib 库,编程效率极低,因此通常都是推荐采用 GTK+ 或 QT 这些对 xlib 进行良好封装的高层次 GUI 库。那几个类型名采用 GLX 为前缀的变量,是那些用于衔接 Xlib 与 GLX 的接口函数所必须的参数。

Step 3: 连接 X Server

任何一个 X 应用程序都是 X 服务器的客户端,因此在编写 X 程序之前,必须要建立它与 X 服务器的连接。这一连接,在 Xlib 中体现为 Display 数据结构(本例中体现为 Step 2 中的 dpy 指针),该结构由 XOpenDisplay 函数返回,即:

        dpy = XOpenDisplay (NULL);

        if (dpy == NULL) {
                printf ("\n\tcannot connect to X server\n\n");
                return -1;
        }

XOpenDisplay 函数所接受的参数是屏幕(显示器)名称字串,其通用格式为“主机名:X 服务器序号.屏幕序号“,例如 "garfileo.m2.org:0.0" 表示位于主机 garfileo.m2.org 上的 0 号 X 服务器的 0 号屏幕。之所以采用这种名称格式来表示显示器,是因为一台主机上可以运行多个 X 服务器,每个 X 服务器可以支持多个屏幕。当 XOpenDisplay 函数的参数为 NULL 时,XOpenDisplay 会读取系统当前的 DISPLAY 环境变量,将它的值作为屏幕名称。

Step 4: 选择合适的 GLX 帧缓冲区配置 (GLX Framebuffer Config)

        fc = glXChooseFBConfig (dpy, 0, att, &nelements);
       
        if (fc == NULL) {
                printf
                    ("\n\tno appropriate framebuffer config found\n\n");
                return -1;
        }

在 Step 1 中,定义了一个 att 的整型数组,数组中包含了若干枚举变量,它们的作用是用于指导 glXChooseFBConfig 函数,让它根据当前的 Display 信息与屏幕信息生成相应的 GLX 帧缓冲区配置。所谓的 GLX 帧缓冲区配置具体体现为 GLXFBConfig 数据结构,其中存储了用于描述 GLXDrawable 的颜色缓冲区及其它从属缓冲区的格式、类型、尺寸等信息。所谓的 GLXDrawable,这里可以简单理解成要实现 OpenGL 图形渲染的 X 窗口。

Step 5: 从 GLX 帧缓冲区配置信息中获得 XVisualInfo

XVisualInfo 数据结构是用来描述 X 窗口所在屏幕的画面格式的。要在 X 窗口中显示 OpenGL 光栅化处理后的图形,必须让 XVisualInfo 能够匹配 GLX 帧缓冲区的配置。GLX 提供了 glXGetVisualFromFBConfig 函数,它可以基于 GLX 帧缓冲区配置以及屏幕信息获得合理的 XVisualInfo 信息,即:

        vi = glXGetVisualFromFBConfig (dpy, *fc);
       
        if (vi == NULL) {
                printf ("\n\tno appropriate visual found\n\n");
                return -1;
        }

Step 6: 创建颜色映射表

在建立 X 窗口之前,需要先为之创建一个合适的颜色映射表。这是因为每个 X 窗口都关联着一个颜色映射表,X 服务器需要根据颜色映射表将像素值转换为真正的颜色值。关于颜色映射表,更为详细的解释见 "Basic Graphics Programming With The Xlib Library" 的 "Color Maps" 一节。

本例中,为即将建立的 X 窗口生成颜色映射表的代码为:

        root = DefaultRootWindow (dpy);
        cmap = XCreateColormap (dpy, root, vi->visual, AllocNone);

这里之所以要使用 DefaultRootWindow 宏从 dpy 中获得根窗口的句柄并将其作为 XCreateColormap 的参数,是因为我们通常希望位于同一根窗口之下的所有窗口都使用相同的颜色映射表。如果每个 X 窗口所使用的颜色映射表都不尽相同,那么很容易会出现显示一个窗口的时候,其它窗口所使用的颜色就发生我们不希望发生的变化。这是因为,X 服务器在同一时间内只能根据一种颜色映射表来指定具体的颜色值(除非显卡支持多个颜色映射表同时工作)。

Step 7: 创建 X 窗口

        swa.colormap = cmap;
        swa.event_mask = ExposureMask | KeyPressMask;

        win = XCreateWindow (dpy, root, 0, 0, width, height,
                                             0, vi->depth,
                                             InputOutput, vi->visual,
                                             CWColormap | CWEventMask, &swa);
        XMapWindow (dpy, win);
        XStoreName (dpy, win, "VERY SIMPLE APPLICATION");

创建 X 窗口之前,需要事先设定 X 窗口属性。X 窗口的属性被封装在一个名为 XSetWindowAttributes 的结构体中,该结构体被作为参数传给 XCreateWindow 函数,然后 XCreateWindow 函数会结合显示屏幕、根窗口、窗口宽高以及屏幕画面格式等信息创建窗口,并返回其句柄。注意,在上述代码中为所建立的 X 窗口设定了 “Exposure(暴露)" 和 "KeyPressMask(摁键)“ 事件支持,后文中会对这两个事件进行响应。

窗口创建完毕后,使用 XMapWindow 将前面所获取的颜色映射表挂接到窗口中,然后使用 XStoreName 设置窗口标题。

Step 8: 建立 OpenGL 渲染环境

        glw = glXCreateWindow (dpy, *fc, win, NULL);
        glc = glXCreateNewContext (dpy, *fc, GLX_RGBA_TYPE, NULL, GL_TRUE);
        glXMakeContextCurrent (dpy, win, win, glc);

上面代码中,glXCreateWindow 创建了支持 OpenGL 图形渲染的 X 窗口,glXCreateNewContext 为这一窗口创建了对应的图形渲染环境,最后由 glXMakeContextCurrent 函数将图形渲染环境绑定到当前的渲染线程。

glXCreateNewContext 函数的最后一个参数(布尔变量)是用来指定 GLX 渲染方式的,如果该参数的值为真,表示采用直接渲染;如果该参数的值为假,则表示采用间接渲染。

Step 9: 初始化 OpenGL 三维场景

        init_scene ();

这一步,实际上与 X Window 无关,仅仅是 OpenGL 编程方面的问题。init_scene 函数是我自己写的,它的工作就是设置 OpenGL 三维场景,譬如设置光源、开启各种缓冲区、设置投影矩阵和模型视图矩阵,具体实现如下:

void
init_scene (void)
{
        GLfloat ambient[] = { 1.0, 1.0, 1.0, 1.0 };
        GLfloat diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
        GLfloat specular[] = { 1.0, 1.0, 1.0, 1.0 };
        GLfloat position[] = { 0.0, 1.0, 1.0, 0.0 };

        /* 设置光源 */
        glLightfv (GL_LIGHT0, GL_AMBIENT, ambient);
        glLightfv (GL_LIGHT0, GL_DIFFUSE, diffuse);
        glLightfv (GL_LIGHT0, GL_SPECULAR, specular);
        glLightfv (GL_LIGHT0, GL_POSITION, position);
        glEnable (GL_LIGHTING);
        glEnable (GL_LIGHT0);
        glEnable (GL_AUTO_NORMAL);
        glEnable (GL_NORMALIZE);

        /* 启用深度测试(隐藏面摘除) */
        glEnable (GL_DEPTH_TEST);

        /* 设置投影矩阵 */
        glMatrixMode (GL_PROJECTION);
        glLoadIdentity ();
        glOrtho (-1., 1., -1., 1., 1., 20.);

        /* 设置模型视图 */
        glMatrixMode (GL_MODELVIEW);
        glLoadIdentity ();
        gluLookAt (0., 0., 10., 0., 0., 0., 0., 1., 0.);
}

Step 10: 窗口事件处理

在 Step 7 中,设定了窗口响应 "Expose" 和 "KeyPress" 事件,在本步骤中对这两个事件的响应如下: 

        while (1) {
                XNextEvent (dpy, &xev);

                if (xev.type == Expose) {
                        XGetWindowAttributes (dpy, win, &gwa);
                        glViewport (0, 0, gwa.width, gwa.height);
                        display ();
                        glXSwapBuffers (dpy, win);
                }

                else if (xev.type == KeyPress)
                        break;
        }

"Expose" 事件是在 X 窗口(客户端)需要重绘时由 X 服务器发送,在本例中,当 X 客户端接到这个事件后,它的响应就是显示 OpenGL 图形。当用户在 X 窗口处于当前状态时进行摁键操作,X 服务器便会想 X 客户端发送 "KeyPress" 事件,在本例中所做的响应就是退出事件处理循环。

上述代码中,对于 "Expose" 事件处理如下:

  1. 获取窗口显示属性;
  2. 根据窗口宽高设置 OpenGL 视口;
  3. 调用 display 函数,绘制 OpenGL 三维图形;
  4. 交换缓冲区。

display 函数是我自己写的,具体实现如下:

void
display (void)
{
        /* 背景 */
        glClearColor (0.2, 0.4, 0.6, 1.0);
        glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        /* 绘制几何体 */
        sphere (1, 0.5f, 100, 100);
}

在 display 函数中,调用了一个绘制球体的函数 sphere,它也是我自己写的,具体实现如下: 

void
sphere (unsigned int solid, double radius, int slices, int stacks)
{
        GLUquadricObj *quadObj = NULL;
        quadObj = gluNewQuadric ();

        if (solid)
                gluQuadricDrawStyle (quadObj, GLU_FILL);
        else
                gluQuadricDrawStyle (quadObj, GLU_LINE);

        gluQuadricNormals (quadObj, GLU_SMOOTH);
        gluSphere (quadObj, radius, slices, stacks);
}

Step 11: 资源释放

当摁键事件发生,程序退出事件循环之后,做一番善后处理,便终止这个程序,具体代码如下:

        glXMakeCurrent (dpy, None, NULL);
        glXDestroyContext (dpy, glc);
        XDestroyWindow (dpy, win);
        XCloseDisplay (dpy);
       
        return 0;
}

完整的程序源文件及编译命令

上述步骤中出现的代码合并为 test.c 文件,采用以下命令进行编译:

$ gcc -lX11 -lGL -lGLU test.c -o test

程序运行结果如下图所示:

 


主要参考文献:

1. OpenGL 红皮书(第 6 版)附录 C.2 节对 GLX 进行了粗略介绍。

2. 《OpenGL Programming for the X Window System》详细地介绍了 OpenGL 如何与 X Window 和 Motif 部件集成,这本书有点老(1999 年出版),从电驴上可以下到,不过很慢,我下了大约 1 个周。

3. GLX 规范,http://www.opengl.org/developers/documentation/glx.html

4. GLX 学习与分析,http://blog.csdn.net/hustwarhd/archive/2007/12/03/1913921.aspx

bones7456 说:
2009年2月10日 18:10

好文啊~不过我还是受A卡所累了,帮忙看看这个问题能解决不?
http://www.linuxsir.org/bbs/thread343988.html

Avatar_small
LiYanrui 说:
2009年2月10日 19:58

我从未用过 A 卡,因此不甚了解它的配置。看了你的帖子,不知道为什么在 xorg.conf 文件中的 module 部分没有 Load "dri" 和 Load "glx"。

bones7456 说:
2009年2月10日 21:08

刚我特意去试了一下,加上 Load "dri" 和/或 Load "glx" 效果都是一样的。。。

chenglong 说:
2009年3月18日 23:32

不错的文章,从你这开始使用glx替换了glut,用来管理窗口和上下文,解决了多线程渲染的问题,感谢下先。

同时,关于《OpenGL Programming for the X Window System》从网上找了好久,看到兄弟下到了,能否帮忙传份电子版,不胜感谢!

邮箱: chenglong365@gmail.com

Avatar_small
LiYanrui 说:
2009年3月19日 00:19

@chenglong:

那本书的电子版,似乎扫描的,体积很庞大,118M,邮箱没法传了。你知道哪个网络硬盘比较好用么?

XXX 说:
2010年9月13日 21:42

@bones7456: 因为glx dri两个extension,如果你没有显示的去disable,在xorg中是默认load的


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter