Syllable
Introduction to device drivers - Part 3
Kernel and AppServer

Video drivers in Syllable are composed of a small kernel driver and a user-space appserver driver. The appserver driver implements the majority of the actual video driver functionality. The kernel driver is used only to access PCI hardware, something that can not be done from user-space. Most video kernel drivers are very similiar. They iterate the list of PCI hardware looking for a supported card, and provide a small set of ioctl() commands that can be used by the appserver driver.

Current video kernel drivers can be found in CVS at /syllable/system/sys/kernel/drivers/graphics and the appserver drivers can be found at /syllable/system/sys/appserver/appserver/drivers/video.

Basic driver model

Appserver drivers are written in C++ and provide methods required to open a video framebuffer, accelerated drawing functions and video overlay controls. The driver can inherit from one of two possible base classes, DisplayDriver or VesaDriver . They are structured like this:

----------------- | MyDriver |----------------- ----------------- | | VesaDriver | |----------------- -----------| DisplayDriver |----------------

The DisplayDriver base class provides unacclerated software rendering functions which can draw a line between two points, draw a filled rectangle or blit a bitmap into video memory. The VesaDriver class is a fully functional VESA 2.0 display driver. It is the default display driver that Syllable will attempt to use if no accelerated display driver can be found for the installed hardware.

Most video drivers inherit from the DisplayDriver class, but some may inherit from the VesaDriver class. The VesaDriver provides additional VESA mode switching functions which may be used for certain hardware. For example the Mach64 accelerated driver inherits VesaDriver in order to use the VESA mode switching functions for some chipsets.

Functionality

Because the DisplayDriver class provides basic software rendering functions, an unaccelerated or partially accelerated video driver does not have to offer hardware drawing for all functions. The most basic display driver can implement only the required functions to detect and initialise the video hardware and allow the appserver to handle all of the video drawing in software.

A more complete display driver can implement hardware accelerated drawing functions by overriding the various drawing methods in the DisplayDriver class. This generally provides much faster video drawing. A complete video driver would also implement the various video overlay functions which can be used by the Media Framework for accelerated video playback.

The basic API

For the purposes of this document we'll pretend we have some video hardware called "Fire" and assume we are writing a display driver for that hardware.

The most basic video driver for any hardware must provide the following functions and methods.

Fire::Fire( int nFd ); Fire::~Fire( void ); bool Fire::IsInitiated( void ) const; area_id Fire::Open( void ); int Fire::GetScreenModeCount( void ); bool Fire::GetScreenModeDesc( int nIndex, os::screen_mode* psMode ); int Fire::SetScreenMode( os::screen_mode sMode ); extern "C" DisplayDriver* init_gfx_driver( int nFd );

The last function in that list is not a C++ method, but instead a C style function. This function is called when the display driver is initialised. Most display drivers simply implement init_gfx_driver() to create a new instance of their display driver class, and then do the actual hardware detection and initialisation in the class constructor. init_gfx_driver() is passed a file handle to the kernel driver, which is can use to call ioctl() and communicate with the kernel driver. The file handle is usually passed to the constructor.

The constructor and destructor should be fairly obvious. Generally the constructor will retrieve the hardware configuration information from the kernel driver. If supported hardware is found then the hardware must be initialised, although this is an internal function of the display driver and will differ between different video hardware. What your initialisation code must do however is create and create an area and remap it to the video framebuffer memory. This area is provided to the appserver later in the initialisation process and is the only way in which the DisplayDriver base class can access the video framebuffer.

Unless your hardware has a functional VESA BIOS and you have inherited from the VesaDriver class, you will have to provide three methods which are used by the appserver to set the video mode. GetScreenModeCount() should simply return the total number of valid screenmodes. GetScreenModeDesc() returns a structure which contains the display mode information for the requested display mode. Finally, SetScreenMode() is used to actually set the desired video mode. GetScreenModeCount() & GetScreenModeDesc() are generally implemented in a similiar maner in any display driver as they are hardware independent. SetScreenMode() is hardware dependent.

The IsInitiated() method simply returns true if the driver was able to detect and initialise the video hardware, or false otherwise.

The Open() method is the last peice of the puzzle. It must return the area ID of the previously created framebuffer area. The appserver can then find the video framebuffer base address from this area and use it to access the video framebuffer to perform drawing functions.

Accelerated drawing

An accelerated video driver will also provide methods which override the DisplayDriver software rendering methods. Their implementation is highly hardware dependent, but most drivers implement methods to accelerate line drawing, rectangular fills and bitmap blits. The methods are:

bool Fire::DrawLine( SrvBitmap *pcBitmap, const IRect &cClipRect, const IPoint &cPnt1, const IPoint &cPnt2, const Color32_s &sColor, int nMode ); bool Fire::FillRect( SrvBitmap *pcBitmap, const IRect &cRect, const Color32_s &sColor, int nMode ); bool Fire::BltBitmap( SrvBitmap *pcDstBitmap, SrvBitmap *pcSrcBitmap, IRect cSrcRect, IRect cDstRect, int nMode, int nAlpha );

All of these methods receive a pointer to a SrvBitmap class. This class is the internal bitmap which is being rendered too. SrvBitmaps can either be in video memory or system memory, depending on wether they are on screen or off screen. Generally, video hardware cannot perform rendering operations on memory which is not in its own video framebuffer, so you must first check to ensure that the bitmap you are rendering to exists in video memory. If it is not, you should pass the rendering request down to your base class, which will use the software rendering methods in DisplayDriver .

SrvBitmap contains a public member called m_bVideoMem which is true if the SrvBitmap is in video memory. Most video drivers implement something similiar to the following:

if( pcBitMap->m_bVideoMem == false ) return( DisplayDriver::FillRect( pcBitMap, cRect, sColor ) );

The DrawLine() and FillRect() methods receive Color information which indicates the color that the line or fill should be drawn with. You may need to convert the RGBA information contained in the Color32_s class to information which can be used by your video hardware, but this is hardware dependent.

The Drawline() and BltBitmap() nMode argument indicates the drawing mode which should be used to perform the operation. This argument will specify DM_COPY (a stright drawing operation), DM_OVER (an alpha transparent "stamp" operation where the transparency is either "On" or "Off") or DM_BLEND (an alpha blending operation). DM_COPY and DM_OVER operations are the most common, and you may choose not to support hardware accelerated DM_OVER and DM_BLEND operations. Generally, passing this drawing operations down to the DisplayDriver methods does not noticably slow down rendering.

Video overlays

If your hardware supports video overlays you may wish to support this functionality in your display driver. There are three methods which you must provide in order to support video overlays correctly. They are:

bool Fire::CreateVideoOverlay( const os::IPoint& cSize, const os::IRect& cDst, os::color_space eFormat, os::Color32_s sColorKey, area_id *pBuffer ); bool Fire::RecreateVideoOverlay( const os::IPoint& cSize, const os::IRect& cDst, os::color_space eFormat, area_id *pBuffer ); void Fire::DeleteVideoOverlay( area_id *pBuffer );

Unlike the rendering functions, these functions do not have a software implementation in the DisplayDriver class. Your video driver must either support video overlays or the user will not be able to use them at all.

CreateVideoOverlay() and DeleteVideoOverlay() are self explanatory, but their implementation is hardware dependent. RecreateVideoOverlay() is used to resize a current video overlay or to change the current color space of a video overlay. It is similar to calling DeleteVideoOverlay() followed by CreateVideoOverlay() .

All of these functions will be highly hardware specific and the functionality is complex. You should refer to actual driver implementations of these methods if you wish to understand how they work.

Off-screen bitmaps

Bliting a bitmap from system memory into video memory can be a slow operation, so it is better to store bitmap data in the hardware video memory where it can be accessed quickly when it is needed. The DisplayDriver class has four memory management functions that help the appserver to manage these off-screen bitmaps. They are:

void InitMemory( uint32 nOffset, uint32 nSize, uint32 nMemObjAlign, uint32 nRowAlign ); status_t AllocateMemory( uint32 nSize, uint32* pnOffset ); SrvBitmap * AllocateBitmap( int nWidth, int nHeight, os::color_space eColorSpc ); void FreeMemory( uint32 nOffset );

The InitMemory() method should be called during intialisation of your driver. It provides important information that the AllocateMemory() method uses to provide memory to the appserver for off-screen bitmaps. The nOffset and nSize arguments give the start and size of the available off-screen video memory. For most hardware the offset would be the first address directly after the end of the largest possible framebuffer. The nMemObjAlign and nRowAlign arguments specify memory alignment values for objects in video memory, and are hardware specific.

If your driver supports video overlays you will also need to call AllocateMemory() and FreeMemory() to manage the video memory required by the video overlay. Your driver should never need to call AllocateBitmap() itself.

To keep everything synchronized there are two additional accelerated rendering methods that you must support. They are:

void LockBitmap( SrvBitmap* pcDstBitmap, SrvBitmap* pcSrcBitmap, os::IRect cSrcRect, os::IRect cDstRect ); void UnlockBitmap( SrvBitmap* pcDstBitmap, SrvBitmap* pcSrcBitmap, os::IRect cSrcRect, os::IRect cDstRect );

LockBitmap() is called by the appserver to ensure that the hardware has completed any accelerated rendering operations. A basic implementation simply waits for the hardware to become idle before it returns. Your hardware may or may not require a more complex method. A simple implementation will not need to implement UnlockBitmap() , but exactly what you must do depends on how you have implemented LockBitmap() .

An example driver

The example appserver driver shows a skeleton appserver video driver, and there is also a matching kernel graphics driver. These are not complete drivers, but are intended to illustrate some of the points covered in this article.