LibGGI: Yet Another Graphics API
We didn't like the idea of another graphics library, but when we checked the available solutions at the time the GGI project was initiated, we found nothing that would fit all our needs:
Transparent acceleration support
First of all, portability is our only weapon against the commercial software market. If we are so portable that we can run on any platform, including the mainstream market, we might be able to get those nice programs, because it is no problem to port them.
The X Window System is about as portable as a program gets and X applications are normally fairly portable. However, using X is often overkill and causes considerable overhead. Moreover, writing X programs is rather difficult (depending on which toolkit you use) and seems really alien to most non-UNIX gurus.
However, X is the most important platform in the UNIX world and, to call ourselves portable, we need to support X. LibGGI uses a system of dynamically loadable target drivers that allow it to run on anything with the ability to display graphics. It does not make a difference if the display target is some type of server software, a KGI-like device, something directly accessing graphics hardware, a printer, a system-service of a microkernel OS or something else. Table 1 shows a few available target systems that LibGGI programs can run on.
X Window System: AIX, IRIX, Solaris, Linux/x86/Alpha
Microsoft Windows (very alpha)
KGI: Linux/x86; planned: Solaris/x86, Linux/Alpha
SVGAlib, GLIDE, SUID-KGI : Linux
LibGGI detects the most desirable target available on the current hardware and automatically makes use of it. This can be overridden to force different behaviour easily.
Compatibility is maintained at binary level within one platform. That is, a LibGGI application compiled for Linux x86 will run without modification on a KGI full screen, in an X window, using SVGAlib or GLIDE. It will even run on a text-mode screen via LibAA or whatever is available.
Compatibility across platforms requires a recompile, but this should be painless if the surrounding code doesn't heavily use OS specifics.
So, porting applications is easy. But what about porting LibGGI itself? We have tried keep LibGGI as portable as possible. Most GGI code compiles without a warning in gcc -pedantic mode. We have also tried to keep use of OS specifics to a bare minimum.
LibGGI should build easily on any system that has heard of POSIX. Even libdl isn't strictly required anymore to allow for systems that don't recognize dynamic libraries.
Another important point in the design of LibGGI is simplicity. If a programmer just wants to write a small graphics utility, he may be scared off by the complexity of X. To give you an idea of how programming with LibGGI works, let's look at the small example program shown in Listing 1.
It doesn't show good style, but is designed to be straightforward to read. As with any library, you have to include its headers. These are located in a subdirectory. Since we have more than one library, we decided that allocating a whole subdirectory would cause less confusion.
The first thing you have to do when using LibGGI is call ggiInit. This initializes the LibGGI internal data structures and sets up everything. Next, you call ggiOpen. This call returns a ggi_visual_t which is an opaque type, similar to what X calls a “drawonable”. Think of it as an abstract handle to the display you draw on. Note that you can have multiple displays per program as required by complex applications which handle multiple screens.
You will want to set a graphics mode on the visual. A mode consists of the size, or rather, resolution of the visible area (visx, visy) as well as that of the virtual area (virtx, virty) on which the view port can be panned around. Moreover, you need to specify the type of display you request; for example, a GT_24BIT true color visual. Note that calls to request additional options are available, as well as the capability to automatically choose values. This is highly recommended in order to enhance portability.
Now we are set to start drawing. LibGGI uses a GC (graphics context) to represent the current state of the drawing system. We considered a state-free approach, but this would have meant:
Lots of parameters for some functions
A very awkward look for programmers used to the GC concept
Ignoring that actual acceleration hardware normally has a GC
We now draw a few dots in different colors by using ggiSetGCForeground and ggiDrawPixel. As an alternative, we draw the next set of pixels using ggiPutPixel. Higher-level functions are also available, but only to a limited extent. As you can see from the example program, we support various kinds of lines and boxes (and yes, these are accelerated, if the underlying target supports it), but that's about it.
Don't be disappointed here. There is a higher-level library called LibGGI2D providing more complicated functions. LibGGI has been designed to be a basic “foundation” library on top of which specialized libraries can be built for more complex requirements, such as 3-D and animation.
When we are done drawing, we use ggiEventPoll to wait for a key or mouse event. ggiEventPoll determines if an event of the given type(s) is present and will eventually block for it for a specified time or indefinitely, if the pointer to the timeval struct is NULL as in our simple case.
We then use a convenience function to get a keystroke. Note that this will block again, if polling was terminated due to mouse activity. In most cases, you will want to use LibGGI's event system to get input from any device that is attachable to a computer system. For event classification and configuration, a helper library called LibGII is available to give you a flexible and simple way of mapping device input to program actions. After ggiGetc has returned, we close down the visual using ggiClose, and then the whole LibGGI library using ggiExit. Note that you can reopen another visual before ggiExit, which can, for example, be used to transfer the program from one target to another. After ggiExit, every other call to LibGGI functions is undefined. You will need to call ggiInit again first. You have now gained a tiny glimpse at how LibGGI programs look.
Many applications, especially those ported from DOS and other systems where a relatively direct access path to the hardware is present, will want to access graphics RAM directly. While being tied to the layout of the particular card/mode isn't a great idea for portability, it is a good way to get extra speed. LibGGI solves this dilemma by exporting a DirectBuffer structure describing all details of the currently active video buffer. The application can decide whether to use it or fall back to standard LibGGI calls.
LibGGI applications can service multiple visuals at the same time, thus allowing multihead applications like CAD or games screens split over several monitors. For convenience, we have “memory-visuals” that can be used to draw an “invisible” area first and then blit to screen (crossblitting). Simple color-space management, such as gamma setting, is available, as well as support for double/triplebuffering and waiting for vertical retrace, or even for a specific position of the CRT beam (where the hardware allows).
LibGGI ends at about the level of a DrawBox, which is not a desirable environment for many applications, and transparent acceleration is limited. LibGGI was kept small on purpose to work well under constrained conditions such as embedded systems, and not waste space for applications which do not need advanced functionality.
We extended LibGGI so more complex APIs could be implemented “on top” of it. So far we have LibGGI2D, Mesa-GGI and a tiny windowing library, LibGWT, running. A lightweight 3-D library, font and animation support are works in progress. Such libraries are implemented as LibGGI-Extensions. Being an extension has several benefits over just “using” LibGGI; for one thing, you inherit the complete functionality regarding library loading and target support. Thus, extension libraries also bring along their set of API drivers which can be used to allow for transparent acceleration. LibGGI ensures basic services, so all extension libraries will run on all LibGGI targets, but the level of acceleration will vary depending on the availability of driver libraries for the extension.
At the core of LibGGI is a trick that helps LibGGI be portable and smart regarding acceleration—a creative usage of dynamically loaded libraries. LibGGI functions can be overridden by loading a suitable library. LibGGI is aware of two different types of such libraries:
Display-Modules describe a way to connect to a given kind of back end like X, KGI, SVGAlib etc. They are loaded at ggiOpen time.
Driver modules are normally loaded at mode-setup time and each describes a given API used to draw on the current target. These APIs are normally selected by the back end that is queried for a set of “suggest-strings” that map to these APIs. See Figure 1.
generic stubs (fallback emulation)
Figure 1. How Transparent Acceleration and Multi-API Work
You might be surprised by the term “set of”. Normally, there are multiple APIs which can be used to draw on a given target. Let me explain this point a bit further for the KGI target, which makes the most extensive use of this feature. Table 2 is the set of suggest-strings for a fictional ABC graphics card being accessed via KGI. The KGI module managing the card will tell LibGGI to first load a “stubs” library that is used for emulation when a function is not natively supported. This stubs library contains fall-back functionality, such as making up a filled box from multiple horizontal lines. Then LibGGI loads a library that accesses the linear, 8-bit wide frame buffer exported by KGI. This library will hold primitives such as DrawPixel and override the stubs library. LibGGI will then load KGI's generic ioctl method to access acceleration features. This library will handle functions which are commonly accelerated. The next suggest-string adds a few commands that are rather uncommon, but present in the ABC, which are accessed by the private area of the ioctls. The last library loaded accesses the ABC registers in an exported MMIO region. All the libraries are loaded in increasing order of precedence. The later ones override functions of earlier ones if they can do better. Please note this is not a static process—it can still change at runtime, if necessary.
When it comes to graphics performance, many people are afraid LibGGI will be slow because of the relatively high level of abstraction its extension libraries provide. Actually, this high level is necessary if we want to use all graphics cards at their maximum capability. Some high-end cards do have a truly high-level internal API. Having applications that use a low-level API would leave that part of the card unused.
On the other hand, in some cases it is difficult to decide which level of API to use. Consider a 3-D game. You can often do some clever optimizations based on your knowledge of the scene. For a low-end graphics card, you might be able to calculate things faster yourself up to the rasterization level. With high-end cards you might be better off using OpenGL directly, because all calculations go to the card which does them faster than the host CPU.
This is a difficult problem, and actually the only good solution is to implement both and select one method at runtime.
Another ever-present problem is calling overhead. It is faster to use inline code than any kind of library. However, the biggest relative gains/losses are achieved with the very fast small operations such as DrawPixel. This is the primary reason we chose to implement DirectBuffer functionality. If the application knows the DirectBuffer format used by the graphics card, it can use its own inline code to bypass the calling overhead.
LibGGI should perform well over the whole range of possible applications and graphics cards, though specialized solutions might perform slightly better.
If you're considering using LibGGI either as a consumer or for programming your own applications, you might be interested in which programs are already available.
LibGGI has been designed for high speed graphics, so game designers are our primary customers. A lot of popular games have been ported to use LibGGI. Descent and DOOM are two of the more well-known ones. Using LibGGI, we managed to run Descent on a Linux-Alpha machine a few weeks after the source was released.
A common misconception about the GGI project is that we are trying to replace X. This is wrong. We are at a much lower layer than a windowing system, and have implemented some popular window systems on top of LibGGI. We have our own X server (Xggi), a viewer application for the VNC networked desktop, and the Berlin consortium is building its server on top of LibGGI. The existence of these servers together with the ability of LibGGI to display on them brings us to the next generation of interoperability.
Another broad group of applications deals with viewing files. LibGGI has the nice ability to view a JPG file on the console or in an X window, without the spawning application (such as mc) being aware that it is running on X or the console.
Most of the above-mentioned programs have been ported from other graphics APIs. All porters have told me that learning LibGGI was easy, and that after porting, the look of the program was improved.
If you are interested in LibGGI, you will want to know where to get it (see Resources). Our project home page provides many pointers and quite a few sources. LibGGI is available as releases from several major software archives like Sunsite and tsx, as daily CVS snapshots from our web page and its mirrors, as well as via CVS from our public CVS mirrors.
Several precompiled binaries are also available, which should be useful for the “pure user” who doesn't want to bother compiling LibGGI. Give LibGGI a try the next time you write a graphics application.