Hi,
The attached patch series is the result of making VirtualGL work with
Mathematica 7.
There are a few issues:
a) M7 tries to XCopyArea from a GLXPixmap-backed Pixmap to a regular
Pixmap; this isn't supported by XCopyArea. This is worked around by
using PutImage to emulate the copy.
b) M7 copies a GLXPixmap to only part of a destination window. This
gives wrong results; you'll get black pixels from the freshly
initialized pbuffer overwriting areas of the window not included in the
copy, because current XCopyArea only knows how to push a full window.
The PutImage path is also used to fix this case.
c) M7 does MakeCurrent(0,0) before calling XCopyArea, also not supported
by the current XCopyArea hook. This is worked around by creating a new
temporary context if necessary.
d) M7 destroys the GLXPixmap before calling XCopyArea on the associated
Pixmap; a questionable practice probably left over from the days when
you could do 2D X drawing over a GLX drawable without problem. This is
worked around by deferring GLXPixmap deletion till Pixmap deletion. (My
NVIDIA driver + X.org gives a race when trying to do the XCopyArea, a
race that M7 always seems to win when run on the console but my test app
can occasionally loose, and VGL always seems to lose.)
That's the context for the following patches. However, I'm mostly
posting them for discussion. I don't really like how ugly XCopyArea is
getting. I think it is better, cleaner, and at least as performant to
just push GLXPixmaps to the 2D X server. (In fact, I think it should
generally be faster, especially since PutImage will be slow for those
not using an X Proxy that compresses.) Also XCopyArea and XCopyPlane
will just work without intervention. As it is we don't support
XCopyPlane at all.
Comments and discussion most welcome. If there are no issues I've
missed, I plan to start down the push-glxpixmap-to-2d-xserver path soon.
-Nathan
P.S. Yes, I really should be including updates to rrfakerut.cpp, but
that's only half-baked at the moment (I tested against the real app).
/*
* A demonstration of what happens when you destroy a GLXPixmap and then
* try to copy out of the corresponding Pixmap.
*
* nk...@opentext.com April 2011
*/
#include <GL/gl.h>
#define GLX_GLXEXT_PROTOTYPES
#include <GL/glx.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static GLXContext ctx;
static XVisualInfo *visinfo;
static GC gc;
static Window make_rgb_window( Display *dpy,
unsigned int width, unsigned int height )
{
const int sbAttrib[] = { GLX_RGBA,
GLX_RED_SIZE, 1,
GLX_GREEN_SIZE, 1,
GLX_BLUE_SIZE, 1,
None };
const int dbAttrib[] = { GLX_RGBA,
GLX_RED_SIZE, 1,
GLX_GREEN_SIZE, 1,
GLX_BLUE_SIZE, 1,
GLX_DOUBLEBUFFER,
None };
int scrnum;
XSetWindowAttributes attr;
unsigned long mask;
Window root;
Window win;
scrnum = DefaultScreen( dpy );
root = RootWindow( dpy, scrnum );
visinfo = glXChooseVisual( dpy, scrnum, (int *) sbAttrib );
if (!visinfo) {
visinfo = glXChooseVisual( dpy, scrnum, (int *) dbAttrib );
if (!visinfo) {
printf("Error: couldn't get an RGB visual\n");
exit(1);
}
}
/* window attributes */
attr.background_pixel = 0;
attr.border_pixel = 0;
/* TODO: share root colormap if possible */
attr.colormap = XCreateColormap( dpy, root, visinfo->visual, AllocNone);
attr.event_mask = StructureNotifyMask | ExposureMask;
mask = CWBackPixel | CWBorderPixel | CWColormap | CWEventMask;
win = XCreateWindow( dpy, root, 0, 0, width, height,
0, visinfo->depth, InputOutput,
visinfo->visual, mask, &attr );
/* make an X GC so we can do XCopyArea later */
gc = XCreateGC( dpy, win, 0, NULL );
/* need indirect context */
ctx = glXCreateContext( dpy, visinfo, NULL, False );
if (!ctx) {
printf("Error: glXCreateContext failed\n");
exit(-1);
}
printf("Direct rendering: %s\n", glXIsDirect(dpy, ctx) ? "Yes" : "No");
return win;
}
static GLXPixmap make_pixmap( Display *dpy, Window win,
unsigned int width, unsigned int height,
Pixmap *pixmap)
{
Pixmap pm, pm2;
GLXPixmap glxpm;
XWindowAttributes attr;
pm = XCreatePixmap( dpy, win, width, height, visinfo->depth );
pm2 = XCreatePixmap( dpy, win, width, height, visinfo->depth );
if (!pm | !pm2) {
printf("Error: XCreatePixmap failed\n");
exit(-1);
}
XGetWindowAttributes( dpy, win, &attr );
/*
* IMPORTANT:
* Use the glXCreateGLXPixmapMESA funtion when using Mesa because
* Mesa needs to know the colormap associated with a pixmap in order
* to render correctly. This is because Mesa allows RGB rendering
* into any kind of visual, not just TrueColor or DirectColor.
*/
#ifdef GLX_MESA_pixmap_colormap
if (strstr(glXQueryExtensionsString(dpy, 0), "GLX_MESA_pixmap_colormap")) {
/* stand-alone Mesa, specify the colormap */
PFNGLXCREATEGLXPIXMAPMESAPROC glXCreateGLXPixmapMESA_func =
(PFNGLXCREATEGLXPIXMAPMESAPROC)
glXGetProcAddressARB((GLubyte *) "glXCreateGLXPixmapMESA");
glxpm = glXCreateGLXPixmapMESA_func( dpy, visinfo, pm, attr.colormap );
}
else {
glxpm = glXCreateGLXPixmap( dpy, visinfo, pm );
}
#else
/* This will work with Mesa too if the visual is TrueColor or DirectColor */
glxpm = glXCreateGLXPixmap( dpy, visinfo, pm );
#endif
if (!glxpm) {
printf("Error: GLXCreateGLXPixmap failed\n");
exit(-1);
}
pixmap[0] = pm;
pixmap[1] = pm2;
return glxpm;
}
int main( int argc, char *argv[] )
{
Display *dpy;
Window win;
Pixmap pm[2];
GLXPixmap glxpm;
XTextProperty windowname;
char *windowtext = "Dest Window";
XStringListToTextProperty(&windowtext, 1, &windowname);
dpy = XOpenDisplay(NULL);
win = make_rgb_window( dpy, 300, 300 );
glxpm = make_pixmap( dpy, win, 300, 300, &pm );
XSync(dpy, False);
XMapWindow( dpy, win );
XSetWMName(dpy, win, &windowname);
glXMakeCurrent( dpy, glxpm, ctx );
printf("GL_RENDERER = %s\n", (char *) glGetString(GL_RENDERER));
/* Render an image into the pixmap */
glShadeModel( GL_FLAT );
glClearColor( 0.5, 0.5, 0.5, 1.0 );
glClear( GL_COLOR_BUFFER_BIT );
glViewport( 0, 0, 300, 300 );
glOrtho( -1.0, 1.0, -1.0, 1.0, -1.0, 1.0 );
glColor3f( 0.0, 1.0, 1.0 );
glRectf( -0.75, -0.75, 0.75, 0.75 );
glFlush();
glXWaitGL();
glXMakeCurrent(dpy, 0,0);
glXDestroyContext(dpy, ctx);
glXDestroyGLXPixmap(dpy, glxpm);
XSync(dpy, False);
printf("do xopy area1\n"); // GLXpm -> nonglx pm
XCopyArea( dpy, pm[0], pm[1], gc, 0, 0, 300, 300, 0, 0 );
printf("do xopy area2\n"); // nonglx pm -> win
XCopyArea( dpy, pm[1], win, gc, 0, 0, 300, 300, 0, 0 );
XSync(dpy, False);
printf("done\n");
fflush(stdout);
while (1) { sleep(1);}
return 0;
}
>From 5a5cda3d5a93ef5c67ac26b4207229d995c78b44 Mon Sep 17 00:00:00 2001
From: nathan.kidd <nathan.kidd@17dcb34f-51d9-0310-863f-d33a0f133fc4>
Date: Wed, 23 Mar 2011 21:13:32 +0000
Subject: [PATCH 04/17] [virtualgl] improved XCopyArea support
Support copying GLX to Pixmap
Fix copying to less than full destination drawable.
Simpler implementation without caching. Room for performance
improvement.
This fix is necessary for Mathematica 7 to work.
git-svn-id: svn://repo/eod/trunk@21065 17dcb34f-51d9-0310-863f-d33a0f133fc4
---
rr/faker.cpp | 99 +++++++++++++++++++++++++++++--
1 files changed, 92 insertions(+), 7 deletions(-)
diff --git a/rr/faker.cpp b/rr/faker.cpp
index 0b552b4..e353a0d 100644
--- a/rr/faker.cpp
+++ b/rr/faker.cpp
@@ -660,22 +660,52 @@ int XMoveResizeWindow(Display *dpy, Window win, int x, int y, unsigned int width
return retval;
}
-// We have to trap any attempts to copy from/to a GLXPixmap (ugh)
-// It should work as long as a valid GLX context is current
-// in the calling thread
+// Window (GLX or not) and Pixmap drawable bits exist in the X Server;
+// a plain XCopyArea will work.
+// Pixmaps with an associated GLXPixmap are a special case since
+// they're never pushed to the X Server (current pbwin/fbx code can't).
+// There are 7 combinations of XCopyArea that involve a GLX-backed Pixmap.
+// "(GLX)Pixmap" means a Pixmap id for which there exists an associated GLXPixmap
+// "(GLX)Window" means a GLXWindow id or a Window id which has been promoted to GLX.
+// 1 (GLX)Pixmap -> Window
+// 2 (GLX)Pixmap -> (GLX)Window
+// 3 (GLX)Pixmap -> (GLX)Pixmap
+// 4 (GLX)Window -> (GLX)Pixmap
+// 5 Pixmap -> (GLX)Pixmap
+// 6 Window -> (GLX)Pixmap
+// 7 (GLX)Pixmap -> Pixmap
+//
+// For 1 a) if the area to be copied is the same size as the destination window we create
+// a GLX pbuffer to back the Window, then use a GL copy and glFinish to flush the pbuffer
+// to the X server's drawable. b) If the area to be copied is smaller than the Window this
+// won't work, because the backing pbuffer is initialized blank and then overwites
+// the parts of the Window not included in the CopyArea. Instead we fall back to a PutImage.
+// For 2 we do the same as 1, except we already have a pbuffer backing both drawables.
+// For 3 and 4 we do a GL copy and don't need to flush since Pixmaps can only be seen
+// by other operations, which are responsible to get the pixels to the X Server.
+// For 5 and 6 we copy to the 2D Pixmap only. Most driver implementation's don't support mixed
+// X (2D) and GLX (3D) drawing anyway, so in general applications shouldn't expect this to work.
+// For 7 we could either teach pbwin/fbx to handle Pixmaps, or, as implemented below,
+// we emulate the copy with glReadPixels and XPutImage
int XCopyArea(Display *dpy, Drawable src, Drawable dst, GC gc, int src_x, int src_y,
unsigned int w, unsigned int h, int dest_x, int dest_y)
{
TRY();
pbuffer *srcpb = NULL, *dstpb = NULL;
+ pbwin *pbw;
GLXDrawable read=src, draw=dst; bool srcpm=false, dstpm=false;
+ bool useputimage = false;
+ bool nonglxsrc2glxpm = false;
if(src==0 || dst==0) return BadDrawable;
if((srcpb=pmh.find(dpy, src))!=0) {read=srcpb->drawable(); srcpm=true;}
if((dstpb=pmh.find(dpy, dst))!=0) {draw=dstpb->drawable(); dstpm=true;}
- if(!srcpm && !dstpm)
+ // Detect case 5 or 6 above: The source is not GLX, destination is GLX Pixmap
+ nonglxsrc2glxpm = (!srcpm && !winh.findpb(dpy, src, pbw) && dstpm);
+
+ if((!srcpm && !dstpm) || nonglxsrc2glxpm)
{
int retval=_XCopyArea(dpy, src, dst, gc, src_x, src_y, w, h, dest_x, dest_y);
return retval;
@@ -693,6 +723,23 @@ int XCopyArea(Display *dpy, Drawable src, Drawable dst, GC gc, int src_x, int sr
if(!fconfig.glp) olddpy=GetCurrentDisplay();
GLXFBConfig config = 0;
int render_type;
+ int glreadformat;
+ XImage *xi;
+ int ignore;
+ unsigned ignoreu, destdrawwidth, destdrawheight;
+
+ // Detect case 7 above: The source is a GLXPixmap, and the destination
+ // is assumed to be a Pixmap (because not GLXPixmap and not a Window we know about)
+ // This isn't 100% reliable: it could be a drawable created by a different
+ // client, but in that case we'll still work.
+ useputimage |= (srcpm && !dstpm && !winh.iswin(dpy, dst));
+
+ // Detect Case 1.b, CopyArea won't cover entire destination Drawable
+ if (!useputimage) // an ugly round-trip delay, so avoid if possible
+ {
+ XGetGeometry(dpy, dst, (Window*)&ignore, &ignore, &ignore, &destdrawwidth, &destdrawheight, &ignoreu, &ignoreu);
+ useputimage |= (dest_x || dest_y || w < destdrawwidth || h < destdrawheight);
+ }
// An application may MakeCurrent(0,0) before calling XCopyArea, but
// could also theoretically try: glBegin, XCopyArea, glEnd, so we only
@@ -720,8 +767,34 @@ int XCopyArea(Display *dpy, Drawable src, Drawable dst, GC gc, int src_x, int sr
return 0;
}
+
+ if (useputimage)
+ {
+ // TODO: consider caching the visual and XImage for performance
+ VisualID vid;
+ XVisualInfo *vis, vtemp;
+ int n;
+
+ vid=_MatchVisual(dpy, config);
+ if(!vid) return 0;
+ vtemp.visualid=vid;
+ vis=XGetVisualInfo(dpy, VisualIDMask, &vtemp, &n); // TODO cache this?
+ if(!vis) return 0;
+
+ xi=XCreateImage(dpy, vis->visual, vis->depth, ZPixmap, 0, NULL, w, h, 8, 0);
+ if((xi->data=(char *)malloc(xi->bytes_per_line*xi->height+1))==NULL)
+ _throw("Memory allocation error");
+ if (render_type == GLX_COLOR_INDEX_TYPE)
+ glreadformat=GL_COLOR_INDEX;
+ else
+ glreadformat=(xi->red_mask&0xff) ? GL_RGBA : GL_BGRA;
+ }
+
// Intentionally call the faked function so it will map a PB if src or dst is a window
- glXMakeContextCurrent(dpy, draw, read, ctx);
+ if (useputimage)
+ glXMakeContextCurrent(dpy, read, read, ctx);
+ else
+ glXMakeContextCurrent(dpy, draw, read, ctx);
unsigned int srch, dsth, dstw;
#ifdef USEGLP
@@ -758,10 +831,22 @@ int XCopyArea(Display *dpy, Drawable src, Drawable dst, GC gc, int src_x, int sr
for(unsigned int i=0; i<h; i++)
{
glRasterPos2i(dest_x, dsth-dest_y-i-1);
- glCopyPixels(src_x, srch-src_y-i-1, w, 1, GL_COLOR);
+ if (useputimage)
+ glReadPixels(src_x, srch-src_y-i-1, w, 1, glreadformat, GL_UNSIGNED_BYTE, xi->data + i*xi->bytes_per_line);
+ else
+ glCopyPixels(src_x, srch-src_y-i-1, w, 1, GL_COLOR);
}
- glFinish(); // call faked function here, so it will perform a readback
+ // Push the image to desktop
+ if (useputimage)
+ {
+ XPutImage(dpy, dst, gc, xi, 0, 0, dest_x, dest_y, w, h);
+ XDestroyImage(xi);
+ }
+ else if (!dstpm) // this is only used by case 1.a and 2
+ {
+ glFinish(); // call faked function here, so it will perform a readback
+ }
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
glMatrixMode(GL_PROJECTION);
--
1.6.3.3
>From 5ca4ea580781d277342639988abcb94ba7c3bb31 Mon Sep 17 00:00:00 2001
From: nathan.kidd <nathan.kidd@17dcb34f-51d9-0310-863f-d33a0f133fc4>
Date: Wed, 23 Mar 2011 21:13:16 +0000
Subject: [PATCH 03/17] [virtualgl] add support to query if a drawable is a window
As opposed to existing methods which query if backed by pbuffer.
Initial purpose is for XCopyArea to figure out drawable types.
git-svn-id: svn://repo/eod/trunk@21064 17dcb34f-51d9-0310-863f-d33a0f133fc4
---
rr/faker-hash.h | 5 +++++
rr/faker-winhash.h | 8 ++++++++
2 files changed, 13 insertions(+), 0 deletions(-)
diff --git a/rr/faker-hash.h b/rr/faker-hash.h
index e0c678a..cbc59c3 100644
--- a/rr/faker-hash.h
+++ b/rr/faker-hash.h
@@ -71,6 +71,11 @@ class _hashclass
return (_hashvaluetype)0;
}
+ bool exists(_hashkeytype1 key1, _hashkeytype2 key2, bool useref=false)
+ {
+ return (findentry(key1, key2) != NULL);
+ }
+
void remove(_hashkeytype1 key1, _hashkeytype2 key2, bool useref=false)
{
_hashclassstruct *ptr=NULL;
diff --git a/rr/faker-winhash.h b/rr/faker-winhash.h
index 5a96c4d..407ade2 100644
--- a/rr/faker-winhash.h
+++ b/rr/faker-winhash.h
@@ -63,6 +63,14 @@ class winhash : public _winhash
else {pbw=p; return true;}
}
+
+ bool iswin(Display *dpy, GLXDrawable d)
+ {
+ if(!dpy || !d) return false;
+ return (0 != _winhash::exists(DisplayString(dpy), d));
+ }
+
+
bool isoverlay(Display *dpy, GLXDrawable d)
{
pbwin *p;
--
1.6.3.3
>From a41684c6bf123aa86e10af971723c1b1055e95a0 Mon Sep 17 00:00:00 2001
From: nathan.kidd <nathan.kidd@17dcb34f-51d9-0310-863f-d33a0f133fc4>
Date: Wed, 23 Mar 2011 21:13:00 +0000
Subject: [PATCH 02/17] [virtualgl] Support XCopyArea even when no current context exists
Some apps, like Mathematica 7, specifically MakeCurrent(0,0) before
calling XCopyArea on a GLX-backed Pixmap.
Note that in case of Mathematica this exposes a pre-existing bug where
XCopyArea(glxpixmap -> pixmap) fails.
---
rr/faker.cpp | 47 +++++++++++++++++++++++++------
1 files changed, 38 insertions(+), 9 deletions(-)
diff --git a/rr/faker.cpp b/rr/faker.cpp
index 894c0c1..0b552b4 100644
--- a/rr/faker.cpp
+++ b/rr/faker.cpp
@@ -667,13 +667,13 @@ int XCopyArea(Display *dpy, Drawable src, Drawable dst, GC gc, int src_x, int sr
unsigned int w, unsigned int h, int dest_x, int dest_y)
{
TRY();
- pbuffer *pb;
+ pbuffer *srcpb = NULL, *dstpb = NULL;
GLXDrawable read=src, draw=dst; bool srcpm=false, dstpm=false;
-
+
if(src==0 || dst==0) return BadDrawable;
- if((pb=pmh.find(dpy, src))!=0) {read=pb->drawable(); srcpm=true;}
- if((pb=pmh.find(dpy, dst))!=0) {draw=pb->drawable(); dstpm=true;}
+ if((srcpb=pmh.find(dpy, src))!=0) {read=srcpb->drawable(); srcpm=true;}
+ if((dstpb=pmh.find(dpy, dst))!=0) {draw=dstpb->drawable(); dstpm=true;}
if(!srcpm && !dstpm)
{
@@ -687,12 +687,37 @@ int XCopyArea(Display *dpy, Drawable src, Drawable dst, GC gc, int src_x, int sr
GLXDrawable oldread=GetCurrentReadDrawable();
GLXDrawable olddraw=GetCurrentDrawable();
- GLXContext ctx=GetCurrentContext();
+ GLXContext oldctx=GetCurrentContext();
+ GLXContext ctx = NULL;
Display *olddpy=NULL;
- if(!ctx || (!fconfig.glp && !(olddpy=GetCurrentDisplay())))
+ if(!fconfig.glp) olddpy=GetCurrentDisplay();
+ GLXFBConfig config = 0;
+ int render_type;
+
+ // An application may MakeCurrent(0,0) before calling XCopyArea, but
+ // could also theoretically try: glBegin, XCopyArea, glEnd, so we only
+ // create a context if necessary, to avoid potential for GLXBadContextState
+ if (oldctx)
+ ctx=oldctx;
+ else
+ {
+ if (srcpb) config = srcpb->config();
+ if (dstpb) config = dstpb->config(); // prefer dst if it exists
+
+ if (config)
+ {
+ if(__vglServerVisualAttrib(config, GLX_RENDER_TYPE)==GLX_COLOR_INDEX_BIT)
+ render_type=GLX_COLOR_INDEX_TYPE;
+ else
+ render_type=GLX_RGBA_TYPE;
+
+ ctx = glXCreateNewContext(dpy, config, render_type, NULL, False); // implementations are not guaranteed to support direct contexts with GLXPixmap
+ }
+ }
+ if(!ctx)
{
stoptrace(); closetrace();
- return 0; // Does ... not ... compute
+ return 0;
}
// Intentionally call the faked function so it will map a PB if src or dst is a window
@@ -745,10 +770,14 @@ int XCopyArea(Display *dpy, Drawable src, Drawable dst, GC gc, int src_x, int sr
glPopClientAttrib();
#ifdef USEGLP
- if(fconfig.glp) glPMakeContextCurrent(olddraw, oldread, ctx);
+ if(fconfig.glp) glPMakeContextCurrent(olddraw, oldread, oldctx);
else
#endif
- _glXMakeContextCurrent(olddpy, olddraw, oldread, ctx);
+ if (oldctx)
+ _glXMakeContextCurrent(olddpy, olddraw, oldread, oldctx);
+ else
+ _glXMakeContextCurrent(_localdpy, None, None, NULL);
+ if (ctx != oldctx) glXDestroyContext(dpy, ctx);
stoptrace(); closetrace();
--
1.6.3.3
>From 6385dcbbf0410726acdd4a3da1e243716ca379b2 Mon Sep 17 00:00:00 2001
From: nathan.kidd <nathan.kidd@17dcb34f-51d9-0310-863f-d33a0f133fc4>
Date: Wed, 23 Mar 2011 21:12:44 +0000
Subject: [PATCH 01/17] [virtualgl] add method to get pbwin's fbconfig
git-svn-id: svn://repo/eod/trunk@21062 17dcb34f-51d9-0310-863f-d33a0f133fc4
---
rr/pbwin.cpp | 2 +-
rr/pbwin.h | 2 ++
2 files changed, 3 insertions(+), 1 deletions(-)
diff --git a/rr/pbwin.cpp b/rr/pbwin.cpp
index 284dd4b..002883b 100644
--- a/rr/pbwin.cpp
+++ b/rr/pbwin.cpp
@@ -104,7 +104,7 @@ pbuffer::pbuffer(int w, int h, GLXFBConfig config)
int pbattribs[]={GLX_PBUFFER_WIDTH, 0, GLX_PBUFFER_HEIGHT, 0,
GLX_PRESERVED_CONTENTS, True, None};
- _w=w; _h=h;
+ _w=w; _h=h; _fbconfig = config;
pbattribs[1]=w; pbattribs[3]=h;
#ifdef SUNOGL
tempctx tc(_localdpy, 0, 0, 0);
diff --git a/rr/pbwin.h b/rr/pbwin.h
index 20b5312..883f22e 100644
--- a/rr/pbwin.h
+++ b/rr/pbwin.h
@@ -34,6 +34,7 @@ class pbuffer
pbuffer(int, int, GLXFBConfig);
GLXDrawable drawable(void) {return _drawable;}
+ GLXFBConfig config(void) {return _fbconfig;}
~pbuffer(void);
int width(void) {return _w;}
int height(void) {return _h;}
@@ -45,6 +46,7 @@ class pbuffer
bool _cleared, _stereo;
GLXPbuffer _drawable;
+ GLXFBConfig _fbconfig;
int _w, _h;
};
--
1.6.3.3
------------------------------------------------------------------------------
Create and publish websites with WebMatrix
Use the most popular FREE web apps or write code yourself;
WebMatrix provides all the features you need to develop and
publish your website. http://p.sf.net/sfu/ms-webmatrix-sf
_______________________________________________
VirtualGL-Users mailing list
VirtualGL-Users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/virtualgl-users