Distributed Multihead X design : Background
Previous: Current issues
Next: Development Results

Appendix

4. Background

This section describes the existing Open Source architectures that can be used to handle multiple screens and upon which this development project is based. This section was written before the implementation was finished, and may not reflect actual details of the implementation. It is left for historical interest only.

4.1. Core input device handling

The following is a description of how core input devices are handled by an X server.

4.1.1. InitInput()

InitInput() is a DDX function that is called at the start of each server generation from the X server's main() function. Its purpose is to determine what input devices are connected to the X server, register them with the DIX and MI layers, and initialize the input event queue. InitInput() does not have a return value, but the X server will abort if either a core keyboard device or a core pointer device are not registered. Extended input (XInput) devices can also be registered in InitInput().

InitInput() usually has implementation specific code to determine which input devices are available. For each input device it will be using, it calls AddInputDevice():

AddInputDevice()

This DIX function allocates the device structure, registers a callback function (which handles device init, close, on and off), and returns the input handle, which can be treated as opaque. It is called once for each input device.

Once input handles for core keyboard and core pointer devices have been obtained from AddInputDevice(), they are registered as core devices by calling RegisterPointerDevice() and RegisterKeyboardDevice(). Each of these should be called once. If both core devices are not registered, then the X server will exit with a fatal error when it attempts to start the input devices in InitAndStartDevices(), which is called directly after InitInput() (see below).

Register{Pointer,Keyboard}Device()

These DIX functions take a handle returned from AddInputDevice() and initialize the core input device fields in inputInfo, and initialize the input processing and grab functions for each core input device.

The core pointer device is then registered with the miPointer code (which does the high level cursor handling). While this registration is not necessary for correct miPointer operation in the current XFree86 code, it is still done mostly for compatibility reasons.

miRegisterPointerDevice()

This MI function registers the core pointer's input handle with with the miPointer code.

The final part of InitInput() is the initialization of the input event queue handling. In most cases, the event queue handling provided in the MI layer is used. The primary XFree86 X server uses its own event queue handling to support some special cases related to the XInput extension and the XFree86-specific DGA extension. For our purposes, the MI event queue handling should be suitable. It is initialized by calling mieqInit():

mieqInit()

This MI function initializes the MI event queue for the core devices, and is passed the public component of the input handles for the two core devices.

If a wakeup handler is required to deliver synchronous input events, it can be registered here by calling the DIX function RegisterBlockAndWakeupHandlers(). (See the devReadInput() description below.)

4.1.2. InitAndStartDevices()

InitAndStartDevices() is a DIX function that is called immediately after InitInput() from the X server's main() function. Its purpose is to initialize each input device that was registered with AddInputDevice(), enable each input device that was successfully initialized, and create the list of enabled input devices. Once each registered device is processed in this way, the list of enabled input devices is checked to make sure that both a core keyboard device and core pointer device were registered and successfully enabled. If not, InitAndStartDevices() returns failure, and results in the the X server exiting with a fatal error.

Each registered device is initialized by calling its callback (dev->deviceProc) with the DEVICE_INIT argument:

(*dev->deviceProc)(dev, DEVICE_INIT)

This function initializes the device structs with core information relevant to the device.

For pointer devices, this means specifying the number of buttons, default button mapping, the function used to get motion events (usually miPointerGetMotionEvents()), the function used to change/control the core pointer motion parameters (acceleration and threshold), and the motion buffer size.

For keyboard devices, this means specifying the keycode range, default keycode to keysym mapping, default modifier mapping, and the functions used to sound the keyboard bell and modify/control the keyboard parameters (LEDs, bell pitch and duration, key click, which keys are auto-repeating, etc).

Each initialized device is enabled by calling EnableDevice():

EnableDevice()

EnableDevice() calls the device callback with DEVICE_ON:

(*dev->deviceProc)(dev, DEVICE_ON)

This typically opens and initializes the relevant physical device, and when appropriate, registers the device's file descriptor (or equivalent) as a valid input source.

EnableDevice() then adds the device handle to the X server's global list of enabled devices.

InitAndStartDevices() then verifies that a valid core keyboard and pointer has been initialized and enabled. It returns failure if either are missing.

4.1.3. devReadInput()

Each device will have some function that gets called to read its physical input. These may be called in a number of different ways. In the case of synchronous I/O, they will be called from a DDX wakeup-handler that gets called after the server detects that new input is available. In the case of asynchronous I/O, they will be called from a (SIGIO) signal handler triggered when new input is available. This function should do at least two things: make sure that input events get enqueued, and make sure that the cursor gets moved for motion events (except if these are handled later by the driver's own event queue processing function, which cannot be done when using the MI event queue handling).

Events are queued by calling mieqEnqueue():

mieqEnqueue()

This MI function is used to add input events to the event queue. It is simply passed the event to be queued.

The cursor position should be updated when motion events are enqueued, by calling either miPointerAbsoluteCursor() or miPointerDeltaCursor():

miPointerAbsoluteCursor()

This MI function is used to move the cursor to the absolute coordinates provided.

miPointerDeltaCursor()

This MI function is used to move the cursor relative to its current position.

4.1.4. ProcessInputEvents()

ProcessInputEvents() is a DDX function that is called from the X server's main dispatch loop when new events are available in the input event queue. It typically processes the enqueued events, and updates the cursor/pointer position. It may also do other DDX-specific event processing.

Enqueued events are processed by mieqProcessInputEvents() and passed to the DIX layer for transmission to clients:

mieqProcessInputEvents()

This function processes each event in the event queue, and passes it to the device's input processing function. The DIX layer provides default functions to do this processing, and they handle the task of getting the events passed back to the relevant clients.

miPointerUpdate()

This function resynchronized the cursor position with the new pointer position. It also takes care of moving the cursor between screens when needed in multi-head configurations.

4.1.5. DisableDevice()

DisableDevice is a DIX function that removes an input device from the list of enabled devices. The result of this is that the device no longer generates input events. The device's data structures are kept in place, and disabling a device like this can be reversed by calling EnableDevice(). DisableDevice() may be called from the DDX when it is desirable to do so (e.g., the XFree86 server does this when VT switching). Except for special cases, this is not normally called for core input devices.

DisableDevice() calls the device's callback function with DEVICE_OFF:

(*dev->deviceProc)(dev, DEVICE_OFF)

This typically closes the relevant physical device, and when appropriate, unregisters the device's file descriptor (or equivalent) as a valid input source.

DisableDevice() then removes the device handle from the X server's global list of enabled devices.

4.1.6. CloseDevice()

CloseDevice is a DIX function that removes an input device from the list of available devices. It disables input from the device and frees all data structures associated with the device. This function is usually called from CloseDownDevices(), which is called from main() at the end of each server generation to close all input devices.

CloseDevice() calls the device's callback function with DEVICE_CLOSE:

(*dev->deviceProc)(dev, DEVICE_CLOSE)

This typically closes the relevant physical device, and when appropriate, unregisters the device's file descriptor (or equivalent) as a valid input source. If any device specific data structures were allocated when the device was initialized, they are freed here.

CloseDevice() then frees the data structures that were allocated for the device when it was registered/initialized.

4.1.7. LegalModifier()

LegalModifier() is a required DDX function that can be used to restrict which keys may be modifier keys. This seems to be present for historical reasons, so this function should simply return TRUE unconditionally.

4.2. Output handling

The following sections describe the main functions required to initialize, use and close the output device(s) for each screen in the X server.

4.2.1. InitOutput()

This DDX function is called near the start of each server generation from the X server's main() function. InitOutput()'s main purpose is to initialize each screen and fill in the global screenInfo structure for each screen. It is passed three arguments: a pointer to the screenInfo struct, which it is to initialize, and argc and argv from main(), which can be used to determine additional configuration information.

The primary tasks for this function are outlined below:

  1. Parse configuration info: The first task of InitOutput() is to parses any configuration information from the configuration file. In addition to the XF86Config file, other configuration information can be taken from the command line. The command line options can be gathered either in InitOutput() or earlier in the ddxProcessArgument() function, which is called by ProcessCommandLine(). The configuration information determines the characteristics of the screen(s). For example, in the XFree86 X server, the XF86Config file specifies the monitor information, the screen resolution, the graphics devices and slots in which they are located, and, for Xinerama, the screens' layout.
  2. Initialize screen info: The next task is to initialize the screen-dependent internal data structures. For example, part of what the XFree86 X server does is to allocate its screen and pixmap private indices, probe for graphics devices, compare the probed devices to the ones listed in the XF86Config file, and add the ones that match to the internal xf86Screens[] structure.
  3. Set pixmap formats: The next task is to initialize the screenInfo's image byte order, bitmap bit order and bitmap scanline unit/pad. The screenInfo's pixmap format's depth, bits per pixel and scanline padding is also initialized at this stage.
  4. Unify screen info: An optional task that might be done at this stage is to compare all of the information from the various screens and determines if they are compatible (i.e., if the set of screens can be unified into a single desktop). This task has potential to be useful to the DMX front-end server, if Xinerama's PanoramiXConsolidate() function is not sufficient.

Once these tasks are complete, the valid screens are known and each of these screens can be initialized by calling AddScreen().

4.2.2. AddScreen()

This DIX function is called from InitOutput(), in the DDX layer, to add each new screen to the screenInfo structure. The DDX screen initialization function and command line arguments (i.e., argc and argv) are passed to it as arguments.

This function first allocates a new Screen structure and any privates that are required. It then initializes some of the fields in the Screen struct and sets up the pixmap padding information. Finally, it calls the DDX screen initialization function ScreenInit(), which is described below. It returns the number of the screen that were just added, or -1 if there is insufficient memory to add the screen or if the DDX screen initialization fails.

4.2.3. ScreenInit()

This DDX function initializes the rest of the Screen structure with either generic or screen-specific functions (as necessary). It also fills in various screen attributes (e.g., width and height in millimeters, black and white pixel values).

The screen init function usually calls several functions to perform certain screen initialization functions. They are described below:

{mi,*fb}ScreenInit()

The DDX layer's ScreenInit() function usually calls another layer's ScreenInit() function (e.g., miScreenInit() or fbScreenInit()) to initialize the fallbacks that the DDX driver does not specifically handle.

After calling another layer's ScreenInit() function, any screen-specific functions either wrap or replace the other layer's function pointers. If a function is to be wrapped, each of the old function pointers from the other layer are stored in a screen private area. Common functions to wrap are CloseScreen() and SaveScreen().

miInitializeBackingStore()

This MI function initializes the screen's backing storage functions, which are used to save areas of windows that are currently covered by other windows.

miDCInitialize()

This MI function initializes the MI cursor display structures and function pointers. If a hardware cursor is used, the DDX layer's ScreenInit() function will wrap additional screen and the MI cursor display function pointers.

Another common task for ScreenInit() function is to initialize the output device state. For example, in the XFree86 X server, the ScreenInit() function saves the original state of the video card and then initializes the video mode of the graphics device.

4.2.4. CloseScreen()

This function restores any wrapped screen functions (and in particular the wrapped CloseScreen() function) and restores the state of the output device to its original state. It should also free any private data it created during the screen initialization.

4.2.5. GC operations

When the X server is requested to render drawing primitives, it does so by calling drawing functions through the graphics context's operation function pointer table (i.e., the GCOps functions). These functions render the basic graphics operations such as drawing rectangles, lines, text or copying pixmaps. Default routines are provided either by the MI layer, which draws indirectly through a simple span interface, or by the framebuffer layers (e.g., CFB, MFB, FB), which draw directly to a linearly mapped frame buffer.

To take advantage of special hardware on the graphics device, specific GCOps functions can be replaced by device specific code. However, many times the graphics devices can handle only a subset of the possible states of the GC, so during graphics context validation, appropriate routines are selected based on the state and capabilities of the hardware. For example, some graphics hardware can accelerate single pixel width lines with certain dash patterns. Thus, for dash patterns that are not supported by hardware or for width 2 or greater lines, the default routine is chosen during GC validation.

Note that some pointers to functions that draw to the screen are stored in the Screen structure. They include GetImage(), GetSpans(), PaintWindowBackground(), PaintWindowBorder(), CopyWindow() and RestoreAreas().

4.2.6. Xnest

The Xnest X server is a special proxy X server that relays the X protocol requests that it receives to a ``real'' X server that then processes the requests and displays the results, if applicable. To the X applications, Xnest appears as if it is a regular X server. However, Xnest is both server to the X application and client of the real X server, which will actually handle the requests.

The Xnest server implements all of the standard input and output initialization steps outlined above.

InitOutput()

Xnest takes its configuration information from command line arguments via ddxProcessArguments(). This information includes the real X server display to connect to, its default visual class, the screen depth, the Xnest window's geometry, etc. Xnest then connects to the real X server and gathers visual, colormap, depth and pixmap information about that server's display, creates a window on that server, which will be used as the root window for Xnest.

Next, Xnest initializes its internal data structures and uses the data from the real X server's pixmaps to initialize its own pixmap formats. Finally, it calls AddScreen(xnestOpenScreen, argc, argv) to initialize each of its screens.

ScreenInit()

Xnest's ScreenInit() function is called xnestOpenScreen(). This function initializes its screen's depth and visual information, and then calls miScreenInit() to set up the default screen functions. It then calls miInitializeBackingStore() and miDCInitialize() to initialize backing store and the software cursor. Finally, it replaces many of the screen functions with its own functions that repackage and send the requests to the real X server to which Xnest is attached.

CloseScreen()

This function frees its internal data structure allocations. Since it replaces instead of wrapping screen functions, there are no function pointers to unwrap. This can potentially lead to problems during server regeneration.

GC operations

The GC operations in Xnest are very simple since they leave all of the drawing to the real X server to which Xnest is attached. Each of the GCOps takes the request and sends it to the real X server using standard Xlib calls. For example, the X application issues a XDrawLines() call. This function turns into a protocol request to Xnest, which calls the xnestPolylines() function through Xnest's GCOps function pointer table. The xnestPolylines() function is only a single line, which calls XDrawLines() using the same arguments that were passed into it. Other GCOps functions are very similar. Two exceptions to the simple GCOps functions described above are the image functions and the BLT operations.

The image functions, GetImage() and PutImage(), must use a temporary image to hold the image to be put of the image that was just grabbed from the screen while it is in transit to the real X server or the client. When the image has been transmitted, the temporary image is destroyed.

The BLT operations, CopyArea() and CopyPlane(), handle not only the copy function, which is the same as the simple cases described above, but also the graphics exposures that result when the GC's graphics exposure bit is set to True. Graphics exposures are handled in a helper function, xnestBitBlitHelper(). This function collects the exposure events from the real X server and, if any resulting in regions being exposed, then those regions are passed back to the MI layer so that it can generate exposure events for the X application.

The Xnest server takes its input from the X server to which it is connected. When the mouse is in the Xnest server's window, keyboard and mouse events are received by the Xnest server, repackaged and sent back to any client that requests those events.

4.2.7. Shadow framebuffer

The most common type of framebuffer is a linear array memory that maps to the video memory on the graphics device. However, accessing that video memory over an I/O bus (e.g., ISA or PCI) can be slow. The shadow framebuffer layer allows the developer to keep the entire framebuffer in main memory and copy it back to video memory at regular intervals. It also has been extended to handle planar video memory and rotated framebuffers.

There are two main entry points to the shadow framebuffer code:

shadowAlloc(width, height, bpp)

This function allocates the in memory copy of the framebuffer of size width*height*bpp. It returns a pointer to that memory, which will be used by the framebuffer ScreenInit() code during the screen's initialization.

shadowInit(pScreen, updateProc, windowProc)

This function initializes the shadow framebuffer layer. It wraps several screen drawing functions, and registers a block handler that will update the screen. The updateProc is a function that will copy the damaged regions to the screen, and the windowProc is a function that is used when the entire linear video memory range cannot be accessed simultaneously so that only a window into that memory is available (e.g., when using the VGA aperture).

The shadow framebuffer code keeps track of the damaged area of each screen by calculating the bounding box of all drawing operations that have occurred since the last screen update. Then, when the block handler is next called, only the damaged portion of the screen is updated.

Note that since the shadow framebuffer is kept in main memory, all drawing operations are performed by the CPU and, thus, no accelerated hardware drawing operations are possible.

4.3. Xinerama

Xinerama is an X extension that allows multiple physical screens controlled by a single X server to appear as a single screen. Although the extension allows clients to find the physical screen layout via extension requests, it is completely transparent to clients at the core X11 protocol level. The original public implementation of Xinerama came from Digital/Compaq. XFree86 rewrote it, filling in some missing pieces and improving both X11 core protocol compliance and performance. The Xinerama extension will be passing through X.Org's standardization process in the near future, and the sample implementation will be based on this rewritten version.

The current implementation of Xinerama is based primarily in the DIX (device independent) and MI (machine independent) layers of the X server. With few exceptions the DDX layers do not need any changes to support Xinerama. X server extensions often do need modifications to provide full Xinerama functionality.

The following is a code-level description of how Xinerama functions.

Note: Because the Xinerama extension was originally called the PanoramiX extension, many of the Xinerama functions still have the PanoramiX prefix.

PanoramiXExtensionInit()

PanoramiXExtensionInit() is a device-independent extension function that is called at the start of each server generation from InitExtensions(), which is called from the X server's main() function after all output devices have been initialized, but before any input devices have been initialized.

PanoramiXNumScreens is set to the number of physical screens. If only one physical screen is present, the extension is disabled, and PanoramiXExtensionInit() returns without doing anything else.

The Xinerama extension is registered by calling AddExtension().

A local per-screen array of data structures (panoramiXdataPtr[]) is allocated for each physical screen, and GC and Screen private indexes are allocated, and both GC and Screen private areas are allocated for each physical screen. These hold Xinerama-specific per-GC and per-Screen data. Each screen's CreateGC and CloseScreen functions are wrapped by XineramaCreateGC() and XineramaCloseScreen() respectively. Some new resource classes are created for Xinerama drawables and GCs, and resource types for Xinerama windows, pixmaps and colormaps.

A region (XineramaScreenRegions[i]) is initialized for each physical screen, and single region (PanoramiXScreenRegion) is initialized to be the union of the screen regions. The panoramiXdataPtr[] array is also initialized with the size and origin of each screen. The relative positioning information for the physical screens is taken from the array dixScreenOrigins[], which the DDX layer must initialize in InitOutput(). The bounds of the combined screen is also calculated (PanoramiXPixWidth and PanoramiXPixHeight).

The DIX layer has a list of function pointers (ProcVector[]) that holds the entry points for the functions that process core protocol requests. The requests that Xinerama must intercept and break up into physical screen-specific requests are wrapped. The original set is copied to SavedProcVector[]. The types of requests intercepted are Window requests, GC requests, colormap requests, drawing requests, and some geometry-related requests. This wrapping allows the bulk of the protocol request processing to be handled transparently to the DIX layer. Some operations cannot be dealt with in this way and are handled with Xinerama-specific code within the DIX layer.

PanoramiXConsolidate()

PanoramiXConsolidate() is a device-independent extension function that is called directly from the X server's main() function after extensions and input/output devices have been initialized, and before the root windows are defined and initialized.

This function finds the set of depths (PanoramiXDepths[]) and visuals (PanoramiXVisuals[]) common to all of the physical screens. PanoramiXNumDepths is set to the number of common depths, and PanoramiXNumVisuals is set to the number of common visuals. Resources are created for the single root window and the default colormap. Each of these resources has per-physical screen entries.

PanoramiXCreateConnectionBlock()

PanoramiXConsolidate() is a device-independent extension function that is called directly from the X server's main() function after the per-physical screen root windows are created. It is called instead of the standard DIX CreateConnectionBlock() function. If this function returns FALSE, the X server exits with a fatal error. This function will return FALSE if no common depths were found in PanoramiXConsolidate(). With no common depths, Xinerama mode is not possible.

The connection block holds the information that clients get when they open a connection to the X server. It includes information such as the supported pixmap formats, number of screens and the sizes, depths, visuals, default colormap information, etc, for each of the screens (much of information that xdpyinfo shows). The connection block is initialized with the combined single screen values that were calculated in the above two functions.

The Xinerama extension allows the registration of connection block callback functions. The purpose of these is to allow other extensions to do processing at this point. These callbacks can be registered by calling XineramaRegisterConnectionBlockCallback() from the other extension's ExtensionInit() function. Each registered connection block callback is called at the end of PanoramiXCreateConnectionBlock().

4.3.1. Xinerama-specific changes to the DIX code

There are a few types of Xinerama-specific changes within the DIX code. The main ones are described here.

Functions that deal with colormap or GC -related operations outside of the intercepted protocol requests have a test added to only do the processing for screen numbers > 0. This is because they are handled for the single Xinerama screen and the processing is done once for screen 0.

The handling of motion events does some coordinate translation between the physical screen's origin and screen zero's origin. Also, motion events must be reported relative to the composite screen origin rather than the physical screen origins.

There is some special handling for cursor, window and event processing that cannot (either not at all or not conveniently) be done via the intercepted protocol requests. A particular case is the handling of pointers moving between physical screens.

4.3.2. Xinerama-specific changes to the MI code

The only Xinerama-specific change to the MI code is in miSendExposures() to handle the coordinate (and window ID) translation for expose events.

4.3.3. Intercepted DIX core requests

Xinerama breaks up drawing requests for dispatch to each physical screen. It also breaks up windows into pieces for each physical screen. GCs are translated into per-screen GCs. Colormaps are replicated on each physical screen. The functions handling the intercepted requests take care of breaking the requests and repackaging them so that they can be passed to the standard request handling functions for each screen in turn. In addition, and to aid the repackaging, the information from many of the intercepted requests is used to keep up to date the necessary state information for the single composite screen. Requests (usually those with replies) that can be satisfied completely from this stored state information do not call the standard request handling functions.


Distributed Multihead X design : Background
Previous: Current issues
Next: Development Results