Kenton Lee

Graphics Effects by Manipulating X Colormaps

byKenton Lee

Published in The X Journal, May, 1992.
Copyright © 1992 Kenton Lee. All rights reserved.

Key words: X Window System, X11, Motif widgets, graphics, colormap, programming.


Abstract

Most X Window System programs use some color graphics. Most of these graphics are generated by traditional raster manipulation techniques. Raster manipulation is easily understood but it, unfortunately, doesn't always give satisfactory results. This tutorial presents some alternatives based on colormap manipulation. Colormap manipulation techniques can be used to supplement, and sometimes replace, raster manipulation techniques. Depending on the application, colormap manipulation may significantly improve performance and/or prevent the undesirable graphical artifacts.

Contents

Introduction

Most X Window System programs use some color graphics. Most of these graphics are generated using traditional raster manipulation techniques (i.e., Xlib drawing functions). With these techniques, if the drawing needs to be changed, all or part of the old drawing is erased and new pieces are drawn.

Raster manipulation is easily understood but it, unfortunately, doesn't always give satisfactory results. This tutorial presents some alternatives based on colormap manipulation. Colormap manipulation techniques can be used to supplement, and sometimes replace, raster manipulation techniques. Depending on the application, colormap manipulation may significantly improve performance and/or prevent the undesirable graphical artifacts. To fully understand this material, you should already understand the raster manipulation techniques.

The two most useful colormap manipulation techniques are dynamic overlay planes and multi-buffered animation. These will be discussed in detail, with example Xlib code. Other techniques are possible, again using standard Xlib functions, and we mention some of them. None of these techniques are new to the computer graphics community, but they may not be well known to X programmers. Most of the techniques were first developed for the interactive CAD systems of the 1970s.[Blinn, Shoup]

Before we get into the code, however, let's review two key X Window System concepts: visual types and colormaps.

Visual Types

X supports many display hardware capabilities. Graphics applications should use the X visual type mechanism to query and select the graphics model most appropriate for your application and hardware. The PseudoColor visual type with a depth of 8 bits per pixel is probably the most popular visual type and the examples in this paper will assume that are using this visual type and that you have specified it correctly. The examples may be easily extended to other visual types with writable colormaps: PseudoColor with other depths, GrayScale, or DirectColor.

If you need more information, [Lemke] is an excellent tutorial on using visual types.

Colormaps

Most popular texts on computer graphics (e.g., [Foley]) or the X Window System (e.g., [Scheifler]) discuss colormaps in detail, so here we present only a brief summary of PseudoColor colormaps.

The PseudoColor model of the graphics display contains writable raster memory and a writable colormap. Raster memory, including the frame buffer, contains a certain number of bits (usually 8) for each pixel displayed on the screen. In X, the values of these bits are called pixel values.

The colormap is a small table with entries specifying the RGB values of the currently available colors. In X, the colormap entries are called color cells. The pixel values are interpreted as indices to the color cells. Thus, to refresh the screen, the display hardware uses the RGB values in the color cells indexed by the pixel values. The size of the colormap is less than or equal to 2^N color cells, where N is the number of bits in a pixel value. A device with 8 bits per pixel value, for example, will have a colormap with up to 256 color cells, indexed though pixel values ranging from 0 through 255.

X provides three ways to access colormaps: shared color cells, standard colormaps, and private color cells. The shared color cell approach is the easiest to use: cells are allocated once with read-only RGB values. The standard colormap approach is a special case of the shared color cells approach, allocating a large number of pre-defined sharable color cells for clients with similar needs. Both the shared color cells and standard colormap approaches conserve color cell resources and should be used whenever possible.[Scheifler, Jones]

There are some powerful graphics techniques, however, that require writable, private color cells. The techniques and examples discussed in this tutorial fall in the third category.

To draw color graphics using the private color cells approach, you first allocate the needed color cells and store the desired RGB values in the color cells. The XAllocColorCells() Xlib function allocates the color cells. Any of XStoreColor(), XStoreColors(), and XStoreNamedColor() can store RGB values in the color cells. Then you can create graphics contexts which use the created colors as foreground or background pixel values, possibly modified by GC functions or plane masks. Finally, you use the graphics contexts to draw into the windows, using rendering functions such as XDrawLine() and XPutImage(). When your display refreshes the screen, it uses the RGB value in the color cell associated with each visible pixel value.

Colormap Manipulation

In many cases, you can generate similar graphics effects by either changing the pixel values (raster manipulation) or by changing the RGB values stored in the color cells (colormap manipulation). The standard X drawing functions change the pixel values. Why should you change the color cells instead instead of the pixel values?

The main differences are memory usage, speed, and graphical artifacts. Many X clients use off-screen pixmaps for generating some graphics. These pixmaps use alot of memory and may not be needed with colormap manipulation techniques. Also, since you usually only have to change a few (or a few dozen) color cells rather than hundreds or thousands of pixel values, you usually get much better performance by changing the color cells.[Blinn] Finally, colormap manipulation can avoid some of the flickering effects usually seen in graphics, especially animations, based raster manipulation.

The disadvantage of the colormap manipulation techniques is that they use at least twice as many color cells compared to raster manipulation techniques. As color cells tend to be valuable resources, this is undesirable. In the examples below, we give more details on the alternatives and their advantages and disadvantages. When designing your application, you have to decide which techniques are most appropriate.

The most important techniques we will be discussing combine colormap manipulation techniques with raster manipulation techniques. The major idea behind these techniques is that the bits in each pixel value can be considered either as a unit or as smaller groups. If considered as a group of values, you can create several independent pictures within your your raster memory, and manipulate the color cells to display combinations of the pictures.

For example, we might draw two independent pictures, one using 7 bits of the pixel value and the other using the remaining 1 bit. Thus, the 8 bit pixel value is divided into:

picture one | picture two ? ? ? ? ? ? ? | ? Let's use XAllocColorCells() to allocate two color cells for picture one: 0 0 0 0 0 0 0 | ? white 1 0 0 0 0 0 0 | ? red And two bits for specifying colors in picture two: ? ? ? ? ? ? ? | 0 white ? ? ? ? ? ? ? | 1 green All graphics drawing in X is controlled through graphics contexts. You use the colormap techniques by specifying the appropriate pixel values, functions, and plane masks in the graphics contexts used in your drawing. First, we create two graphics contexts for drawing. The first draws picture one: foreground: 10000000 background: 00000000 function: GXcopy plane mask: 11111110 The second draws picture two: foreground: 00000001 background: 00000000 function: GXcopy plane mask: 00000001 After drawing, the resultant pixel values will be some of the following: 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 1 1 0 0 0 0 0 0 | 0 1 0 0 0 0 0 0 | 1 What picture do we get in our window? That depends on the colors stored in the color cells in the current colormap. We can change the colors at will with XStoreColors(). This colormap displays picture one, no matter what is in picture two with this colormap: 0 0 0 0 0 0 0 | 0 white (background) 0 0 0 0 0 0 0 | 1 white (background) 1 0 0 0 0 0 0 | 0 red (foreground) 1 0 0 0 0 0 0 | 1 red (foreground) We can display only picture two with this colormap: 0 0 0 0 0 0 0 | 0 white (background) 0 0 0 0 0 0 0 | 1 green (foreground) 1 0 0 0 0 0 0 | 0 white (background) 1 0 0 0 0 0 0 | 1 green (foreground) With this colormap, we get both pictures, with picture two overlapping picture one. 0 0 0 0 0 0 0 | 0 white (background) 0 0 0 0 0 0 0 | 1 green (picture 2 foreground) 1 0 0 0 0 0 0 | 0 red (picture 1 foreground) 1 0 0 0 0 0 0 | 1 green (picture 2 foreground, overlaps picture 1) More complex effects are also possible. Below, we discuss techniques for managing multi-color drawings and for erasing without disturbing overlapping drawing.

Note that if other applications have already allocated color cells from the default colormap, your application may not be able to allocate enough for these techniques. In these cases, you will get an error from the allocation function and you may want to create a separate colormap. The ICCCM discusses techniques for installing your own colormap.[Rosenthal]

Overlay Planes

One very useful colormap manipulation technique is the overlay plane.[Blinn] You may have generated a complex drawing, then want to draw some other (usually much simpler) graphics above it. Using a simple raster manipulation drawing model, the overlay graphics corrupt the base drawing, so when you erase the overlay, you have to redraw the base drawing, which can be very compute expensive, as well as ugly.

The typical overlay plane example is an air traffic control system, with a static map display and moving air craft icons drawn over the map. Another example, from the CAD world, is an interactive base drawing with active areas that are highlighted only when the mouse is within them. You can probably think of many other examples for your application area.

Overlay plane applications usually divide the 8 bit pixel values into 7 bits for the base drawing and 1 bit for the overlay plane. The key to this technique is giving all pixels in the overlay plane (i.e., pixels with the overlay plane bit set to one) the same color. Thus, drawing or erasing in the overlay plane is simply a matter of setting or clearing this single bit.

Assuming the visual type and colormap are set correctly, the XAllocColorCells() function is used to specify up to 128 writable base color cells for the base drawing and specify one plane mask:

#define PLANES 1 /* number of overlay planes */ #define PIXELS 3 /* number of base colors */ unsigned long planeList[PLANES], pixelList[PIXELS]; status = XAllocColorCells(dpy, cmap, False, planeList, PLANES, pixelList, PIXELS); Please remember that the number of color cells actually allocated is not C, where C is the number of base colors, but (C * 2^P), where P is the number of planes. The status will tell us if we could allocate the correct color cells. If not, a new colormap is probably necessary.[Rosenthal]

Assuming it was successful, we store colors in the allocated cells. We could use XStoreColor(), but XStoreNamedColor() makes the example a little clearer. Again, the key is setting all pixels in the overlay plane are given the same color.

int flags = DoRed|DoGreen|DoBlue; XStoreNamedColor(dpy, cmap, "white", pixelList[0], flags); XStoreNamedColor(dpy, cmap, "green", pixelList[1], flags); XStoreNamedColor(dpy, cmap, "yellow", pixelList[2], flags); XStoreNamedColor(dpy, cmap, "blue", pixelList[0] | planeList[0], flags); XStoreNamedColor(dpy, cmap, "blue", pixelList[1] | planeList[0], flags); XStoreNamedColor(dpy, cmap, "blue", pixelList[2] | planeList[0], flags); One disadvantage of XStoreNamedColor() is that it will generate an X protocol error if the color name is not known to your X server. To avoid these errors, you could use XLookupColor() to validate the names, choosing others if the name is not known.

The graphics contexts for drawing the base graphics are created normally:

XGCValues xgcv; unsigned long mask = GCForeground | GCBackground | GCFunction; xgcv.function = GXcopy; xgcv.background = pixelList[0]; xgcv.foreground = pixelList[1]; baseGC = XCreateGC(dpy, win, mask, &xgcv); The graphics contexts for the overlay graphics use the plane mask attribute to modify (set or clear) the overlay plane bit only. To draw, we use the GXset function. Again, this works since all pixel values with the overlay plane set generate the overlay color: mask = GCFunction | GCPlaneMask; xgcv.function = GXset; xgcv.plane_mask = planeList[0]; overlayDraw0GC = XCreateGC(dpy, win, mask, &xgcv); To erase, we use the GXclear function: mask = GCFunction | GCPlaneMask; xgcv.function = GXclear; xgcv.plane_mask = planeList[0]; overlayClear0GC = XCreateGC(dpy, win, mask, &xgcv); This technique is easily extended to handle more than one overlay plane. Note, however, that each overlay plane doubles the number of color cells required. To specify more than one overlay plane, first request that number of planes in the XAllocColorCells() request. If that succeeds, set color values for all combinations of pixels and overlay plane masks. For example, if two overlay planes are used, you would have to specify these colors, in addition to those specified above: XStoreNamedColor(dpy, cmap, "red", pixelList[0] | planeList[1], flags); XStoreNamedColor(dpy, cmap, "red", pixelList[1] | planeList[1], flags); XStoreNamedColor(dpy, cmap, "red", pixelList[2] | planeList[1], flags); XStoreNamedColor(dpy, cmap, "red", pixelList[0] | planeList[0] | planeList[1], flags); XStoreNamedColor(dpy, cmap, "red", pixelList[1] | planeList[0] | planeList[1], flags); XStoreNamedColor(dpy, cmap, "red", pixelList[2] | planeList[0] | planeList[1], flags); The last three colors, where both planes are specified, are used when the two overlay drawings overlap (i.e., both overlay plane bits are set). Any color could be used, depending on the effect desired for the overlap, though most applications will want the same color as one of the overlay drawings.

The graphics contexts for drawing and erasing the second overlay plane are created the same way as for the first, using the second plane in the plane_mask:

mask = GCFunction | GCPlaneMask; xgcv.function = GXset; xgcv.plane_mask = planeList[1]; overlayDraw1GC = XCreateGC(dpy, win, mask, &xgcv); mask = GCFunction | GCPlaneMask; xgcv.function = GXclear; xgcv.plane_mask = planeList[1]; overlayClear1GC = XCreateGC(dpy, win, mask, &xgcv); Note that both clearing graphics contexts clear the overlay planes independently, even where the overlay drawings overlap. If necessary, you could also create a graphics context that cleared both planes at once by specifying both planes in the plane_mask attribute: mask = GCFunction | GCPlaneMask; xgcv.function = GXclear; xgcv.plane_mask = planeList[0] | planeList[1]; overlayClearBothGC = XCreateGC(dpy, win, mask, &xgcv); We leave at it as an exercise for the reader to find create uses for the graphics contexts we have created.

Double Buffer Animation

Double (or multi-) buffering is used by most high quality animation applications. The technique is similar to the overlay plane technique described above. In double buffering, you draw two pictures in the window, but manipulate the colormap to make only one of them visible. Each picture can have several colors, usually the same set of colors in each picture. To get the animated effect, you modify the second, hidden picture, then use XStoreColors() to quickly change the colormap, displaying the second picture and hiding the first, and repeat. Because XStoreColors() works very quickly and the drawing process is hidden from the user, the animation is very fast and smooth. The alternative raster manipulation technique is sometimes slower and usually generates undesirable flickering or other graphical side effects as the drawing is created.

Some X servers support a protocol extension that provides multi-buffering functionality. Your server may not have this extension or you may choose not to use it for portability reasons. Below, we describe the two buffer (double buffering) case, though this example is easily extended to any number of buffers, up to the number of bits per pixel.

We could use the overlay plane approach, described above, to create the hidden image, but this requires one overlay plane per color, which is very expensive. The technique described here allows 2^P colors, where P is the number of color planes used, e.g., three planes allows the animation of up to eight colors.

In this example, we assume that there are two drawings, each with three colors. Three colors requires 2 planes, with one plane combination unused. Assume that the allocated base pixel values are (binary) 01, 10, and 11 and the allocated planes are the last two. To display the first picture and hide the second, we must assign these colors to the pixel values:

0 1 0 0 0 0 | any white 1 0 0 0 0 0 | any yellow 1 1 0 0 0 0 | any red We have allocated a total 12 pixel values (C * 2^P). Since any could be (binary) 00, 01, 10, or 11, we store these 12 colors in the color cells: 0 1 0 0 0 0 | 0 0 white 0 1 0 0 0 0 | 0 1 white 0 1 0 0 0 0 | 1 0 white 0 1 0 0 0 0 | 1 1 white 1 0 0 0 0 0 | 0 0 yellow 1 0 0 0 0 0 | 0 1 yellow 1 0 0 0 0 0 | 1 0 yellow 1 0 0 0 0 0 | 1 1 yellow 1 1 0 0 0 0 | 0 0 red 1 1 0 0 0 0 | 0 1 red 1 1 0 0 0 0 | 1 0 red 1 1 0 0 0 0 | 1 1 red To hide the first picture and display the second, we use the opposite colormap: any | 0 0 white any | 0 1 yellow any | 1 0 red any | 1 1 unused or: 0 1 0 0 0 0 | 0 0 white 0 1 0 0 0 0 | 0 1 yellow 0 1 0 0 0 0 | 1 0 red 0 1 0 0 0 0 | 1 1 unused 1 0 0 0 0 0 | 0 0 white 1 0 0 0 0 0 | 0 1 yellow 1 0 0 0 0 0 | 1 0 red 1 0 0 0 0 0 | 1 1 unused 1 1 0 0 0 0 | 0 0 white 1 1 0 0 0 0 | 0 1 yellow 1 1 0 0 0 0 | 1 0 red 1 1 0 0 0 0 | 1 1 unused The code for the double buffered animation is similar to the overlay plane code. First, allocate the twelve color cells: #define PLANES 2 /* planes for combinations */ #define PIXELS 3 /* base pixel values */ status = XAllocColorCells(dpy, cmap, False, planeList, PLANES, pixelList, PIXELS); Again, if this fails, a new colormap is needed.

You could use XStoreNamedColor(), as in the overlay plane example, to store the colors, but since the colors will be changed repeatedly during the animation process, a more efficient method is to create two vectors of color values (one for the first picture and one for the second) and install one of the vector all at once with XStoreColors(). Since XStoreColors() requires RGB values instead of color names, we first convert a list of color names to a list of RGB values. The RGB values are stored in XColor structures:

XColor exact; /* not used */ XColor colorDefs[PIXELS]; /* list of RGB values */ static char names[] = { "white", "yellow", "red"}; for (i = 0; i < PIXELS; i++) { status = XLookupColor(dpy, cmap, names[i], &exact, colorDefs + i); if (status == 0) { printf("could not find color %s\n", names[i]); exit(1); } } Here, we print an error message and exit if the color name is not found. An alternative is to try to use a different color.

Next, we create the two XColor vectors, representing the two sets of twelve color cells, as shown above. One helpful hint is to put all the data for each picture, including the vectors and the graphics contexts, into abstract data types. Switching pictures is then simply a matter of switching a pointer to the data.

#define TOTAL 12 /* PIXELS * 2^PLANES */ typedef struct picture { /* abstract data type */ XColor map[TOTAL]; /* color map vectors */ GC draw0GC, draw1GC, clearGC; /* graphics contexts */ struct picture *other; } PICTURE; PICTURE p1, p2, *current = &p1; /* vector 1 */ for (cell = i = 0; i < PIXELS; i++) { XColor *c = colorDefs + i; copyColor(c, p1.map + (cell++), pixelList[i]); copyColor(c, p1.map + (cell++), pixelList[i] | planeList[0]); copyColor(c, p1.map + (cell++), pixelList[i] | planeList[1]); copyColor(c, p1.map + (cell++), pixelList[i] | planeList[0] | planeList[1]); } /* vector 2 */ for (cell = i = 0; i < PIXELS; i++) { copyColor(colorDefs + 0, p2.map + (cell++), pixelList[i]); copyColor(colorDefs + 1, p2.map + (cell++), pixelList[i] | planeList[0]); copyColor(colorDefs + 2, p2.map + (cell++), pixelList[i] | planeList[1]); copyColor(colorDefs + 0, p2.map + (cell++), pixelList[i] | planeList[0] | planeList[1]); } We store the first color in the unused plane combination. If we later find that we need to use that combination, the code is easy to modify. This utility function was useful for copying XColor definitions and assigning pixel values: void copyColor(def, color, pixel) XColor *def, *color; unsigned long pixel; { color->red = def->red; color->green = def->green; color->blue = def->blue; color->flags = DoRed | DoGreen | DoBlue; color->pixel = pixel; } Because we want to be able to draw to either picture without disturbing the other, our graphics contexts are slightly more complex than in the overlay plane example. All of the graphics contexts now have plane masks to prevent corruption from one picture to the other. Also, the GXcopy function is used in all of the graphics contexts, giving more control over the resultant drawing. Again, we store the graphics contexts in the abstract data type, allowing easy access from the drawing routines. /* create plane masks */ unsigned long used; for(j = 0, used = 0L; j < PLANES; j++) used |= planeList[j]; /* map 1 */ xgcv.plane_mask = ~ used; mask = GCFunction | GCPlaneMask | GCForeground; xgcv.function = GXcopy; xgcv.foreground = pixelList[1]; p1.draw0GC = XCreateGC(dpy, win, mask, &xgcv); xgcv.foreground = pixelList[2]; p1.draw1GC = XCreateGC(dpy, win, mask, &xgcv); xgcv.foreground = pixelList[0]; p1.clearGC = XCreateGC(dpy, win, mask, &xgcv); /* map 2 */ mask = GCFunction | GCPlaneMask | GCForeground; xgcv.plane_mask = used; xgcv.function = GXcopy; xgcv.foreground = planeList[0]; p2.draw0GC = XCreateGC(dpy, win, mask, &xgcv); xgcv.foreground = planeList[1]; p2.draw1GC = XCreateGC(dpy, win, mask, &xgcv); xgcv.foreground = 0L; p2.clearGC = XCreateGC(dpy, win, mask, &xgcv); The first defined color, pixelList[0], is used both for the window background and for clearing the graphics. The other two colors, pixelList[1] and pixelList[2], are used for foreground graphics. Further graphics context attributes, such as background colors, line widths, stipples, etc. could be specified in the usual way for more complex drawings.[Scheifler]

Finally, to switch pictures, we switch color cell definitions:

XStoreColors(dpy, cmap, hidden->map, TOTAL); Where hidden is a pointer to the currently hidden PICTURE abstract data type.

Again, our reader exercise is to use these graphics contexts to create interesting applications.

Colormap-only Techniques

While overlay planes and double buffering are the two most important colormap manipulation graphics techniques, others may also be useful to some applications. We will briefly discuss two others, color cycle animation and alternate color animation, which are useful in some applications. Their implementations are much simpler than the above examples, so we will not present example code. Both are pure colormap manipulation techniques; the raster graphics are drawn once and all other graphical effects come solely from manipulating the colormap. They have the advantage of being very fast, but the disadvantage of being very inflexible.[Shoup]

Color cycle animation is one of the simplest colormap manipulation techniques. It is most frequently used in video games and simulations where a flashy effect is needed. One application of color cycling is a "sparkling" or "fluid" effect where pixels in a window are drawn with random pixel values, then the colors associated with the color cells are rapidly cycled. The colors are typically cycled simply by shifting them by one pixel value per iteration and storing with XStoreColors(). Some simple motion object effects can also be generated by assigning the same pixel values to larger areas, such lines or rectangles.

Similar to color cycling is alternate color animation. With this technique, only two unique colors are used, foreground and background. The background color is set to be the same as the window background color. Several instances of a single object are drawn in several different locations, with the different location representing different positions along the path of the object's animation. Each instance is drawn with a different pixel value. To achieve the animation, use XStoreColors() or XStoreNamedColor() to assign the foreground color to exactly one of the pixel values. As the foreground color is remapped to different instances' pixel values, the object will appear to move.

Both color cycling and alternate color animation can be extended to handle many objects, overlapping objects, etc. with some restrictions. The major restriction is that you cannot change the path of the animation once the object instances have been drawn. For more details, see [Shoup]. If you need a dynamic path, you must use the overlay plane or multi-buffer techniques described above.

The X Toolkit

While few, if any, popular X Toolkit widgets use these colormap manipulation techniques for their graphics, you can easily use them in your own graphics widgets. The color allocation and mapping functionality should be implemented in these widgets' initialization and set-values class methods.

Conclusion

In conclusion, colormap manipulation techniques are very powerful and easily implemented using standard Xlib functions. They can greatly improve the performance of some applications and remove unwanted graphical artifacts from others. They do not always replace the more popular raster manipulation techniques, but do supplement them well. Colormap manipulation techniques do require, however, many more color cells than do raster manipulation techniques that use shared color cells or standard colormaps, so those techniques should also be considered.

The examples presented in this tutorial assume an 8 bit per pixel PseudoColor visual type. You should be able to easily extend them to use any visual type with a writable colormap.

References

[Blinn]
James Blinn, "Raster Graphics", in Tutorial: Computer Graphics, edited by Kellogg Booth, IEEE Computer Society Press, 1979.
[Foley]
Foley, van Dam, Feiner, and Hughes, Computer Graphics: Principles and Practice, second edition, Addison-Wesley, 1990.
[Jones]
Oliver Jones, Introduction to the X Window System, Prentice-Hall, 1989.
[Lemke]
David Lemke and Davis Rosenthal, "Visualizing X11 Clients," Proceedings of the Summer, 1988 USENIX Conference.
[Rosenthal]
David Rosenthal, "Inter-Client Communication Conventions Manual," in [Scheifler].
[Scheifler]
Robert Scheifler and James Gettys, X Window System, second edition, Digital Press, 1990.
[Shoup]
Richard Shoup, "Color Table Animation", SIGGRAPH'79 Conference Proceedings.

THE AUTHOR

Kenton Lee is an independent software consultant specializing in X Window System and OSF/Motif software development. He has been developing UNIX graphical user interface software since 1981.

Ken has published over two dozen technical papers on the X Window System. Most are available over the World Wide Web at http://www.rahul.net/kenton/bib.html.

Ken may be reached by Internet electronic mail to kenton @ rahul.net or the World Wide Web at http://www.rahul.net/kenton/.


[HOME] For more information on the X Window System, please visit my home page..


Please send me your comments on this paper:

Name: E-mail:

[X Consulting] [Home] [Mail] [X Papers] [X WWW Sites]