The XFree86 Architecture

In this column, Alan Cox has written many good introductions on how to write kernel drivers for various types of hardware, from mice to SCSI boards to radio interfaces to video cards. However, it's not just the kernel that needs good drivers for new hardware -- with the increasing focus on Linux on the desktop, hardware support for XFree86 is just as important.

Gearheads Figure 1
Figure One:XFree86 version 4′s new module loader facilitates loading drivers, extensions, and other code at runtime.

In this column, Alan Cox has written many good introductions on how to write kernel drivers for various types of hardware, from mice to SCSI boards to radio interfaces to video cards. However, it’s not just the kernel that needs good drivers for new hardware — with the increasing focus on Linux on the desktop, hardware support for XFree86 is just as important.

For more than ten years now, the X Window System has been the standard graphics interface on Unix machines. About a year after Linus released the first versions of the Linux kernel, X had been ported to Linux as well. Actually, a large part of the work was to port Linux to X, that is, to add missing features to the kernel.

Since then a lot has changed, on both the software and hardware side. In the early days of Linux, few people dared to think how widely it would soon be used. Back in 1992 there were just a handful of companies designing different versions of dumb frame buffers, all of which were extensions of IBM’s VGA design. These used a lousy bus interface (ISA), and were extremely limited in basically every feature: color depth, maximum screen resolution, maximum refresh rate, and so forth.

The original XFree86 (which was based on Thomas Röll’s X386 code) was designed for exactly this target hardware — dumb cards that extended the VGA standard. The assumptions based on this target specification were deeply spread throughout the design of XFree86′s drivers. As a result, adding new features and utilizing the abilities of today’s graphics hardware became harder and harder over the years.

The days of ISA cards and dumb framebuffers are over. Today’s PCI and AGP busses allow easy recognition of cards (if only the vendors would better utilize the possibilities they could have in using the PCI config space; but that’s a different story). Today’s cards concentrate more computing power in the graphics engine than the average CPU had just a few years ago. So, while it is still possible to simply map the framebuffer and have the CPU draw pixels there, the real fun in getting new cards supported, of course, lies in the wide variety of accelerated drawing operations that are available.

In early 1997, The XFree86 Project decided to redesign the way drivers for XFree86 were implemented. This was an insane idea, considering how much code had been developed for the old driver interface and how much work it would be to redo all the infrastructure from scratch. Honestly, had we known how much work it would be, we might have never started.

Nevertheless, we forged ahead, and with XFree86-4.0 in March 2000 and the recent XFree86-4.0.1 in July, the results of this work were available to the public. Now what we need are good, optimized drivers for this new architecture. Many are already available, and the list of supported hardware is quite a long one. A few old cards are missing, and all of the new hardware that comes out will continue to need drivers, and that’s what I would like to make easier to get started with in this article.

New Module Loader

Let’s look at some of the fundamentals behind XFree86 version 4. One important new feature that actually makes it easier to provide new drivers is the new module loader. The base server binary can load drivers, font renderers, extension and other code at runtime. Figure One shows a graphic representation of this.

This is similar to the kernel’s module loading mechanism, but was extended with a nice twist: XFree86 modules are, in general, OS independent. So unless the module itself uses certain kernel features (as some 3D or high-end 2D driver modules do), you can use the very same module not only under Linux, but also under FreeBSD, NetBSD, or even Solaris. This means writing an XFree86 driver module covers more than just Linux.

Several different object formats are supported for loadable modules. The most frequently used format is ELF, which is the standard format generated by Linux compilers. In order to open such a module, the server calls:

LoadModule(ModuleName, Path, SubDirs, Pattern,
OptionList, ModReq,&errmaj, &errmin)

This allows the server to load the given module, finding it in the SubDirs of Path, according to the given Pattern. The options given in the OptionList are passed to the setup function of the module when the module is first initialized. The loader framework can ensure that the required ABI versions given in ModReq are met.

Which modules are loaded at runtime is determined in three ways: through compiled-in defaults (e.g., a bitmap font renderer is mandatory to be loaded); through the XF86Config file (e.g., when certain extension or additional font renderers are requested); and through dependencies among the modules (e.g., when a driver module requests the generic frame buffer code to be loaded).

The connection between the symbol spaces is a little bit tricky, but not too hard to get used to. First, modules are not supposed to directly reference any symbols from system libraries. This makes it easier to write portable modules that can be used across OSs. Second, the module can only use symbols from the main server binary that have been explicitly exported to this module. The server exports the wrappers around standard C library calls (like xf86open() instead of open() — there is a header file xf86_libc.h that makes using these wrappers transparent to the developer) plus several of its internal interfaces that allow modules to hook into the structures of the X server. Lastly, the module may reference symbols exported from other modules that are loaded into the server as well.

In order to hook the module into the server, right after opening a module (and before the relocation of any symbols) the server will be looking for a data entry called <ModuleName>ModuleData. For example, Mark Vojkovich’s Matrox module (mgaModuleData) is used below:

static MODULESETUPPROTO(mgaSetup);

static XF86ModuleVersionInfo mgaVersRec =
ABI_CLASS_VIDEODRV, /* This is a video driver */

XF86ModuleData mgaModuleData = { &mgaVersRec, mgaSetup, NULL };

This structure includes versioning information, as well as function pointers that the server binary calls to initialize and (in case it is unloaded) de-initialize the module. The setup function is very important when you are planning to write a driver. This is the function that hooks the new driver into the server data structures and, therefore, makes the functionality of the driver available. Again, the Matrox driver gives a good example, as you can see in Listing One (pg. 91).

The important part (after making sure that this function is only called once) is to add a structure describing the driver to the list of drivers using xf86AddDriver(). The DriverRec structure that is passed to xf86AddDriver() contains version and naming information on this driver, plus three function pointers: a function which can be used to print out the name of this driver, a function which allows this driver to probe for the hardware it supports, and a function which lists the option names (in the configuration file) this driver recognizes.

With these functions the server can now announce that it has loaded this module, parse the corresponding part of the configuration file, and finally ask the module to probe for supported hardware. If the module recognizes hardware in the system that it supports, it then will create the full set of data structures that are needed for an XFree86 graphics card driver.

Loading a Driver

Let’s take a quick walk through the crucial pieces you will need in order to get a very simple driver going. This example just shows you how to set up a framebuffer and a display video mode; several volumes could probably be written about the inner workings of things like resource control, acceleration of X Windows, or even 3D.

We already mentioned the Identify() function that just prints out a message identifying the driver. The Probe() function is a lot more interesting, as it is central to driver startup; it allows the driver to figure out where to find the hardware it supports and how to access it. While this information can be provided in the configuration file, and must be so specified in a multi-head environment, the driver still needs to register its use of that hardware.

In order for drivers to be able to co-exist and to make sure that they don’t disturb other hardware in the system, the probe needs to be as careful as possible. No registers may be modified and ideally (for current hardware) the PCI IDs should be used to identify whether the driver supports the hardware installed or not.

One other important step in the Probe() function is the beginning of the initialization of the ScrnInfoRec data structure. It contains pointers to all the other relevant driver functions, some of which are explained below.

Next comes a PreInit() function that still should not make any changes to the hardware, but may probe the hardware more thoroughly in order to collect all the information to set up the hardware. This would include things like the exact type and revision of the graphics chipset or the amount of video memory installed (if not specified in the configuration file).

At the time PreInit() is called, we already know that the driver supports the hardware that was found, so this is a good time to load and initialize other generic parts of the server that we want to use. For example, we might load the vgahw module (which provides useful functions for hardware which is VGA-compatible), the generic framebuffer module, and the XAA module (used by accelerated hardware). For most of the initialization tasks, moreover, helper functions are available either in the core server or in an auxiliary module (such as vgahw).

The first function that should modify the settings of a device is ScreenInit(). Here the selected graphics mode is set up and the remaining server data structures are filled. The framebuffer routines are initialized, and a few other initial setup routines are called.

Most drivers provide some additional functions to better layer the work done in ScreenInit(). Normally Save() and Restore() are available to provide an easy way to save and restore the complete video state of the graphics board. And often a ModeInit() function is available that encapsulates the setup of a new video mode.

EnterVT() and LeaveVT() are called before switching from the server to another virtual terminal (LeaveVT) and after coming back to the server (EnterVT). SaveScreen() is used to blank and un-blank the screen. Finally, CloseScreen() resets the graphics card to its original state and frees allocated data structures in the driver. It is called at the end of a server generation (e.g., when the X server is killed, but also when it is restarted).

For More Information

As you can tell by now, this article can only be a quick peek at the inner workings of an XFree86 graphics driver. Much more detail is needed to get started, and even more to get all the pieces together and for the first time see the new board initialize the famous initial X-weave. But trust me, seeing that for the first time on a new card is worth the effort.

Quite a few additional resources are available in order to get there. The best information available as a document is the DESIGN document that is part of the XFree86 sources. When XFree86 is installed you can find it in /usr/X11R6/ lib/X11/doc/README, in the sources it can be found as xc/ programs/Xserver/hw/xfree86/doc/DESIGN.

The XFree86 sources are available from our public CVS server. Directions can be found at http://www.xfree86.org/cvs/. Alternatively, all the sources can be found on our ftp server at ftp://ftp.xfree86.org/pub/XFree86/4.0.1/.

And of course there is the XFree86 discussion forums with hundreds of people who know the inner workings of XFree86 and will be happy to help people working on new drivers. Information on how to join the various XFree86 mailing lists can be found at http://xfree86.org/mailman/listinfo.

This article is intended to give you a quick introduction into the new XFree86 architecture. If there is sufficient interest, a small series of articles going into more detail and through a step-by-step description can be added.

Listing One: The Matrox Driver

#define MGA_VERSION 4000
#define MGA_DRIVER_NAME “mga”

DriverRec MGA = {

static pointer
mgaSetup(pointer module, pointer opts, int *errmaj, int *errmin)
static Bool setupDone = FALSE;

/*This module should be loaded only once, but check to be sure. */

if (!setupDone) {
setupDone = TRUE;
xf86AddDriver(&MGA, module, 0);

* Modules that this driver always requires maybe
* loaded here by calling LoadSubModule().

* Tell the loader about symbols from other modules that
* this modulemight refer to.
LoaderRefSymLists(vgahwSymbols, cfbSymbols, xaaSymbols,
xf8_32bppSymbols, ramdacSymbols,
ddcSymbols, i2cSymbols, shadowSymbols,
fbdevHWSymbols, vbeSymbols,
#ifdef XF86DRI
drmSymbols, driSymbols,


* The return value must be non-NULL on success even though
* there is no TearDownProc.
return (pointer)1;
} else {
if (errmaj) *errmaj = LDR_ONCEONLY;
return NULL;

Dirk Hohndel is Vice President of The XFree86 Project, Inc. and member of the XFree86 Core Team. In his other life he is also known as Chief Technology Officer of SuSE Linux AG. Dirk can be reached at hohndel@suse.de.

Comments are closed.