Categories: Linux
Intro
The simplest code-path for displaying graphics on a Linux system is by using a “framebuffer driver”. This article describes how a Linux framebuffer driver works.
The article here is for (beginner) kernel developers, or those just curious how linux graphics work. It might be a little interesting for linux sysadmins as background info. And a total waste of time for general users, or even userspace application developers.
For information about the whole linux graphics stack, see this article.
Accessing Graphics
There are 3 different ways that graphics can be performed:
- Using a kernel framebuffer driver
- Using a kernel DRM driver
- Mapping the card’s control registers into user-space (via
mmap()
calls on/dev/mem
), and using user-space code to access the card (very complex)
What is a Framebuffer Driver?
It is a Linux kernel driver which exposes a file /dev/fb{N}
for a graphics card it is responsible for. Through that file, userspace apps can perform reads/writes to directly access the video card framebuffer - ie the pixel values being displayed on the screen. Via IOCTL calls on that file, other functionality can be accessed including:
- setting the graphics mode (width, height, pixel-depth, etc)
- passing bitmaps to be copied into video memory
- passing coordinates of a rectangle to be filled with a specified colour
The framebuffer interface does not offer much in the way of acceleration other than the bitblits and fills mentioned above.
There is a fairly generic “vesafb” driver that is capable of controlling any graphics card compatible with the VESA standard. Linux also comes with dozens of card-specific framebuffer drivers that support products of specific manufacturers. On boot, Linux uses the PCI ID of a graphics card to load the most appropriate framebuffer driver, which in turn creates the /dev/fb{n}
control file.
Why Use a Framebuffer Driver?
Embedded systems use them because they often don’t have complex graphics requirements, want a small kernel, and don’t want an X server running in userspace. While X can use a framebuffer to display graphics, so can other simpler graphical libraries.
For many graphics chips, there is a framebuffer driver available but no specific X driver, and no 3D driver.
Graphics display during startup is also a candidate for a framebuffer driver.
And linux text consoles can use a framebuffer driver to display text at nice resolutions (better than available via the BIOS) without otherwise actually wanting to display graphics.
Framebuffers and DRM/KMS
The was originally a dedicated kernel framebuffer driver module for most cards; this driver would provide the /dev/fb*
node used by userspace to interact with this driver.
For many cards, there is now a “DRM” (Direct Rendering Manager) driver which provides “KMS” (Kernel Modesetting) functionality, supports the framebuffer interface, and optionally provides interfaces needed to do accelerated 3D rendering (GEM/TTM and other features). When the DRM kernel driver for a card is loaded, it provides the /dev/fb*
node - ie opening that file creates a channel to the multi-purpose DRM driver rather than the single-purpose framebuffer driver; this makes coordination between multiple users of the graphics card (X servers, and direct framebuffer users such as virtual terminals) better. However in many cases the standalone framebuffer driver code is still maintained, for cases where the complexity of a DRM driver is not wanted (eg embedded systems).
The X Server and the Framebuffer
The X server predates the concept of a Linux kernel framebuffer driver, ie the original X usermode graphics drivers directly implement their own modesetting and access to the card framebuffer memory. In addition, the framebuffer interface is a Linux-specific concept while X runs on multiple operating systems.
The userspace-based approach to accessing graphics was used by the X server for many decades; few other applications have tried this as (a) maintaining the necessary card-specific code is very complex, (b) the application must run as ‘root’ to have the right to use /dev/mem
, and (c) integration with other users of the graphics card is tricky. As an example of (c), the boot console and virtual consoles (ie those accessed via ctrl-alt-F{n}
) usually use a framebuffer driver. To solve (c), a tricky dance is needed to keep things working. The framebuffer driver configures graphics when it is loaded (shortly after boot) and provides the traditional text bootup messages (may be hidden when using a modern distro that uses something like Plymouth to display a pretty bootsplash screen by default). When X starts, its drivers save the current graphics state, then start directly poking the graphics control registers to set things up the way it wants. Obviously, if anything tries to then use the framebuffer driver then things will get ugly as X has changed settings “behind the back” of the framebuffer driver; however this normally doesn’t happen. If the user presses the magic ctrl-alt-F{n}
sequence to switch to a text console, then X detects this and restores the original saved graphics state before allowing the (framebuffer-based) text console to run. When the user then presses ctrl-alt-f{n}
to switch back to X, X needs to reinstate its graphics settings. These reinitialisations of the graphics card cause switching to be slow, and the screen to “flicker”.
The X server on Linux now mostly uses user-space drivers which rely on a DRM driver that provides KMS; as the same driver also provides the framebuffer interface that driver has all the knowledge needed to correctly arbitrate between X and framebuffer users, and the above problem is resolved. The same driver handles both APIs, and therefore knows what modes are selected regardless of which API was used. Switching between framebuffer-using applications (such as a text console) and X applications then does not need to reinitialise the graphics (as no sneaky changes occur behind the back of the driver).
The X server can alternatively use a generic userspace driver that relies on the framebuffer API, rather than a card-specific driver. X then relies on the framebuffer to do modesettings, and does all rendering in software; it then passes the rendered screen to the graphics card using either write() or the bitblit IOCTL. In this mode, X can’t do much acceleration.
Linux Driver Source
Directory drivers/video holds a implementations of the framebuffer interface for a truckload of different chips. Among them are some generic drivers:
- “vesafb.c” can drive any VESA2.x compliant chip
- “vga16.fb” can drive any VGA-compliant chip
- “xen-fbfront” can be used by virtual machines running on Xen hypervisor; presumably framebuffer graphics calls get routed to the Xen hypervisor which then allows the administrator to display that appropriately.
There are also drivers for playstation3 graphics, atari onboard graphics, cirrus, sparc, trident, etc.
The vesa driver is one of the simplest drivers, and below we will walk through some of the internal implementation.
VesaFB
File drivers/video/vesafb declares module_init(vesafb_init)
, ie an init function which is executed on module load. This:
-
Parses any driver options provided by
mod_probe
-
Invokes
platform_probe
which calls back intovesafb_probe(platform_device)
. The “platform” part is because VESA is accessed via the BIOS; a framebuffer driver for a PCI card would instead register itself with the PCI framework, which then calls back into the device’s probe function passing astruct pci_dev
. -
Invokes request_mem_region which checks to see whether any other driver has already reserved the specified hardware memory address range (fixed in the case of VESA, more dynamic for PCI); if not, then the requested range is registered as “reserved”. See macro
include/linux.ioport.h:request_mem_region
->kernel/resource.c:__request_region
->__request_resource
. -
allocates a
framebuffer_info
structure (seefbsysfs:framebuffer_alloc
) followed by N bytes of buffer for the color palette values (info->par
). -
Invokes
request_region
to try to reserve the historical range of I/O address ports for VESA. The driver doesn’t actually care if it fails, as it doesn’t use them. However it’s elegant to mark them, as this info appears in the sysfs structure representing IO ports, and traditionally VESA has this specific range. -
Optionally sets up MTRRs (Memory Type Range Registers) to mark the hardware address range region (reserved above by
request_mem_region
) as having the appropriate cacheing behaviour. Appears to be somewhat card or system dependent, so driver parameters are available to tweak it. Defaults to “write-through”, ie the slowest but safest setting for volatile memory accesses. -
uses ioremap function to create a kernel virtual address mapping for the hardware address of the graphics framebuffer (with appropriate caching flags set).
-
stores an fbops (“framebuffer operations”) structure into the
framebuffer_info
, containing pointers to callback functions. The callbacks are used from the generic framebuffer support code; in particular, many of these get invoked as the result of a user IOCTL call. Callbacks include:- imageblit // accelerate copying images from userspace to graphics framebuffer
- copyarea // copy data around within the existing framebuffer
- fillrect // fill a rectangular area in the framebuffer with a specific colour (eg blank the screen!)
-
calls infrastructure function
fbmem.c:register_framebuffer()
to register the /dev/fb{n} file, hook up write and IOCTL methods etc.
FBMem module
Several files in this directory (fbmem.c, fb_sys_fops.c, fbsysfs.c) are common code that are compiled into their own kernel module. This module doesn’t expose functionality to the user itself, just acts as “library code” for other drivers. Although it is a module, it is usually statically linked into the kernel, and therefore does not appear in the “modules.dep” dependency list for the vesafb.ko module even though vesafb.c invokes functions exported from these files via EXPORT_SYMBOL macros.
Module Init
File fbmem has module_init(fbmem_init)
which:
-
defines an array of registered framebuffers (
registered_fb
) - initially empty. -
registers file /proc/fb - which can be read to determine what framebuffer is currently “active”
- reserves major device# FB_MAJOR for later use, but doesn’t create any /dev nodes yet. It also associates a file_operations structure with that major device#; the major callbacks are:
- syscall open –> fb_open
- syscall ioctl –> fb_ioctl
- syscall mmap –> fb_mmap
- syscall write –> fb_write
- syscall read –> fb_read
- uses class_create to create sysfs entry /sys/class/graphics. When drivers call framebuffer_register, then a child sysfs node is created which automatically creates a symlink under /sys/class/graphics.
The FB_MAJOR major device number is used when creating /dev/fb{n} nodes later in calls to the register_framebuffer()
function; those nodes then inherit the settings of the parent major device - ie when the user performs open/read/write/mmap/ioctl on the /dev/fb{n}
node, the handler functions for the major device# are invoked. They then delegate to the callback functions in the framebuffer_info->fbops
structure if defined, otherwise perform the default behaviour.
The default fb_ioctl
function allows the user to:
- get the current graphics mode settings (screen resolution etc) [FBIOGET_VSCREENINFO]
- request the current graphics mode settings be changed [FBIOPUT_VSCREENINFO]
- read/write the color palette [FBIOGETCMAP/FBIOPUTCMAP]
- read/modify which virtual consoles are on which framebuffer devices [FBIOGET_CON2FBMAP/FBIOPUT_CON2FBMAP]
The default fb_mmap
function uses io_remap_pfn_range to map the graphics framebuffer into the virtual memory space of the calling userspace application, and returns the userspace address at which the data has been mapped. The userspace app can then use normal reads/writes to access the graphics framebuffer.
The default fb_write
function just copies data from userspace to the graphics framebuffer (ie assumes caller has the layout correct). This implementation uses an intermediate buffer, repeatedly copying a page worth of data from userspace into the buffer, then from there to the real framebuffer using include/linux/fb.h:fb_memcpy_tofb
which maps to memcpy_io (for x86). Many drivers override this operation to instead use common function fb_sys_fops.c:fb_sys_write
which does the copy in one go.
The default fb_read
function just copies data from the graphics framebuffer to userspace.
So in short, for video devices which have a fairly normal framebuffer layout, userspace just uses FBIOGET_VSCREENINFO
to figure out what the layout is, then generates an appropriate image and does standard file-writes against the /dev/fd{N}
file. These map to direct copies into the videocard memory. Alternatively, userspace can use mmap() to get access to the framebuffer addesses and then just memcpy its prepared pixel data into the address range returned by the mmap call. For video devices with odd IO-mappings or odd framebuffer layouts, the fbops->fb_write
callback can be overridden to manually handle userspace->videocard data translation; presumably such drivers provide a custom mmap callback that simply rejects mmap() calls (return an error).
Modesetting for framebuffer devices is done via an IOCTL of type FBIOPUT_VSCREENINFO
. As noted above, “KMS” drivers include framebuffer functionality.
The “fbset” commandline tool uses these IOCTLs to display/update the current graphics mode. Note that the “randr” tool is a front-end for X, not the framebuffer - it just sends a network message to X, which then asks its current driver to set the selected mode.
The strucures and constants used in IOCTL calls from userspace are defined in include/linux/fb.h
.
The core filebuffer code also broadcasts “notifications” when certain significant changes occur to a framebuffer; other code can register as a listener for these events. In particular, this is used by the framebuffer-console code (described later).
The Logo
File fbmem.c also has quite a lot of code for displaying the penguin “logo” on the boot console (which is usually a framebuffer console). The logo is normally embedded in the kernel image. Code exists to show muliple copies of it (which indicate #CPUs), and to show it rotated 90 or 180 degrees.
A remarkable amount of code is dedicated to this feature…
Registering a Framebuffer
During initialisation, a framebuffer driver (eg vesafb) will call fbmem.c:register_framebuffer
. This:
-
creates
/dev/fb{N}
using FB_MAJOR (as registered byfbmem_init
). This device therefore inherits thefile_operations
callbacks defined for the major#. -
stores a pointer to the registered driver in the
registered_fb
array (and incrementsnum_registered_fb
) -
allocates an 8bk block of ram for “pixmap”
-
calls
fb_init_device
to create a sysfs entry with lots of control functions..
File fb_sys_fops
This file provides convenient implementations for read/write syscalls on the /dev/fb{N}
operation, which simply
copies bytes between userspace buffers and the memory-mapped video buffer set up by the framebuffer driver.
It presumably assumes that the user data is in the appropriate format already.
As noted above, there is a default implementation of read/write/etc in file fbmem.c. These alternate implementations can be used by a driver simply by setting the appropriate member in its ops structure (which the fbmem code checks before calling its default).
File cfbimgblt
This file provides implemenations for copying images from userspace to video memory, with optional
format changes (eg colorspace). Takes an fb_image
parameter as the source.
Sysfs interface
fb_init_device
(fbsysfs.c) creates an entry in sysfs, with control files for things like:
-
show_bpp
/store_bpp
-
show_blank
/store_blank
-
show_console
/store_console
-
show_cursor
/store_cursor
-
show_mode
/store_mode
-
show_modes
/store_modes
-
show_pan
/store_pan
-
show_virtual
/store_virtual
show_name, show_stride
-
show_rotate
/store_rotate
(screen rotation, eg 90%) -
show_fbstate
/store_fbstate
-
show_bl_curve
/store_bl_curve
(backlight for LED screens)
The other functions in this file handle reads or writes of those sysfs files.
The Framebuffer Console Driver
Linux has the concept of a “tty driver” for text input and output. The implementation of various TTY devices can be found in drivers/tty. In particular, drivers/tty/vt defines the concept of “virtual terminals” : having multiple concurrent sessions on a single computer, and switching between them using magic key sequences.
The interface that a kernel module must implement in order to register itself as a TTY is defined in include/linux/console.h:struct consw
.
File drivers/video/console/fbcon.c implements a “text console interface” API on top of a framebuffer device. Actually, it supports up to NR_CONSOLES
independent consoles which might be mapped to the same or different framebuffer devices. In effect, it implements the “consw” interface defined for /dev/tty{N}
, and uses a framebuffer device as the output medium. Type “struct consw” is declared in include/linux/console.h
and defines an API like:
-
con_init(..)
// initialise a caller-provided datastructure as console#N -
con_putc(..)
// write a single character at the current cursor location -
con_scroll(..)
// scroll a section of the screen by N lines
It creates a sysfs entry for fbcon, and adds sysfs entries so user can query and update some graphical output stuff like screen rotation or cursor size. User writes to these sysfs files are equivalent to IOCTL calls on the framebuffer device node itself - but more convenient. There is only one entry for all the fbcon consoles; properties are bound to the “currently active” fbcon console.
Useful sysfs nodes include:
- /sys/devices/graphics/fbcon
- /sys/devices/virtual/graphics/fbcon
The fbcon module attaches itself as a listener on framebuffer events, in order to make sure it keeps “in sync” with changes to framebuffers, eg resolution changes.
Framebuffer “instances” and console “instances” have only a loose binding; there is effectively a matrix that specifies which console is to be displayed on which framebuffer. Array con2fbmap holds this data, ie indicates for console#N which framebuffer# it is attached to, eg con2fbmap[3]=1
means that console#3 is on framebuffer#1. A value of -1 means “not attached to any framebuffer”. On start, the first-registered framebuffer is considered the “primary”, and all possible consoles (NR_CONSOLES
) are mapped to this framebuffer. Of course, most machines only have one framebuffer (actually, max# of consoles can be reduced via module parameter “vc” to less than NR_CONSOLES
).
It is the TTY system that creates the /dev/tty{n}
files, not fbcon. The fbcon code then “takes over” responsibility for consoles using function drivers/tty/vt/vt.c:take_over_console
. Any further IOCTL operations on this file are then redirected to the “struct consw” hooks defined by fbcon.
Userspace Tools
The fbset
command can be used to change graphics modes via IOCTL operation FBIOPUT_VSCREENINFO on /dev/fb{n} files.
Other Notes
The memory buffer that the graphics card scans repeatedly in order to generate signals to the display (the framebuffer) is also known as the “scan out buffer”.
References
See also:
- http://people.freedesktop.org/~marcheu/linuxgraphicsdrivers.pdf
- Linux Device Drivers, Third Edition (Corbet, Rubini, Kroah-Hartman) http://lwn.net/Kernel/LDD3/