Many languages and tools are available for building graphical applications, but the goal is always the same: to fill the frame buffer with the right bits as the right time. The next chapter investigates this at a high level, but this chapter concentrates on low-level programming. The example applications respond to the display’s synchronization signals and set pixels directly inside the frame buffer.
The thought of programming at this low level makes many nervous, but accessing the Linux frame buffer is simple. Once you understand what the frame buffer is and how it works, you can control its operation with one function. That is, you can set every color in every pixel in your display. Beats “Hello World!” if you ask me.
As I write this, the PlayStation 3 remains the most accessible and inexpensive Cell-based system available. Most of this chapter describes the Linux frame buffer in general, but parts of the discussion are specific to Sony’s frame buffer API. The example code in this chapter runs only on the PlayStation 3 console.
Sony has made the PS3 frame buffer API available, but there are no details available on how to interface the console’s RSX graphics processor. For more information about this and other low-level PS3 topics, I recommend you visit ps2dev.org.
Before getting into the details of Linux devices, let’s start with an even simpler topic: your computer’s/console’s graphical display. Whether your display is a cathode-ray tube (CRT) monitor, a liquid crystal display (LCD) monitor, or just a television, there are basic terms that describe its operation.
This section explains these terms and then describes Linux devices in general. I’ll discuss how Linux devices are accessed through device files, and then show how this file access makes it possible to draw pixels into a frame buffer.
For the purposes of this chapter, a display’s viewing screen is divided into rows. Each row is divided into light points or picture elements, abbreviated pixels. This is shown in Figure 19.1.
The display’s resolution determines the number of rows and pixels available. If a screen’s resolution is 1024 × 768, there are 1024 pixels per row and 768 rows, for a total of 786,432 pixels.
On digital displays, the number of colors a pixel can take is determined by the display’s color depth, or the number of bits per pixel. My monitor is Truecolor, which means each pixel stores 8 bits for red, 8 bits for green, and 8 bits for blue, for a total of 24 color bits per pixel and 224 = 16,777,216 possible colors. Many monitors go far beyond this and provide color depths of 36 and 48 bits.
Whether it involves striking phosphors with an electron beam or charging capacitors in a grid, each pixel is drawn at a speed set by the pixel clock or dot clock. This is usually expressed in millions per second, or MHz. If a monitor has a maximum pixel clock of 100MHz, each pixel is drawn in 1/100,000,000 sec, or 10 µs.
Monitors generally draw pixels in rows. The top row is drawn first, and after it’s finished, a horizontal sync signal starts the process of drawing the row beneath it. The number of horizontal sync signals generated per second is called the horizontal sync frequency, which is usually between 30 and 90KHz.
Once all the rows are drawn, a vertical sync signal restarts the process of drawing the screen, starting from the top row. The number of vertical sync signals per second is the vertical sync frequency or the vertical refresh rate. This is necessarily lower than the horizontal sync frequency, and common values lie between 20 and 150Hz.
This vertical sync signal is important for this chapter, because the PlayStation 3 transfers pixel data from the computer to the display each time it’s generated. The section of computer memory that holds this pixel data is called the frame buffer.
If you look in the /dev directory, you’ll see the device files recognized by your Linux installation. Each provides a file-based interface to a hardware device or a pseudo-device. The naming conventions are similar from Linux installation to installation: lp
represents a printer device and hd
represents an IDE disk.
By using file descriptors, you can access these devices as if they were regular Linux files. But there are two important differences:
Device files allow for underlying device access through the I/O control (ioctl
) function.
Device files send and receive data differently depending on whether they are character devices or block devices.
The ioctl
function is explained shortly. First, you have to see how to interface character devices and block devices.
A character device, such as a serial modem or virtual terminal, receives data one character at a time. Applications can write to character devices immediately because there’s no need to package the characters in groups. Similarly, read operations receive one character at a time in sequence. There are no addresses and no way to access data at random locations.
A block device, such as a DVD-ROM or hard disk, can only send and receive data in fixed sizes called blocks. Input and output to these devices can’t be performed immediately, but must wait until the data fills a buffer of a specific size. Block devices are generally addressable and allow random access through seek operations.
The Linux frame buffer is a block device file whose bits are transferred to and from a display device. Frame buffers are designated with the prefix fb
in the /dev directory. The default display device is commonly named fb0
. Frame buffers transfer data in blocks called frames, and each frame contains all the bits needed to draw an entire display.
/dev/fb0 can be accessed like a regular file. For example, to send pixel data from the frame buffer into pixels.dat, you can use a simple cat
command:
cat /dev/fb0 > /tmp/pixels.dat
The frame buffer can also be memory mapped to user memory with code such as the following:
fd = open("/dev/fb0", O_RDWR); addr = mmap(NULL, length, PROT_WRITE, MAP_SHARED, fd, 0);
Here, length
is the amount of memory in the frame buffer. After executing this code, writing to the addr
memory region updates pixel data in the frame buffer. The following command fills the frame buffer with 0s:
memset(addr, 0, length);
Modern frame buffers, such as the PS3 frame buffer, have enough memory to hold multiple frames at once. While the contents of one frame are sent to the display, the application fills the contents of another. When the new frame data is ready, the application tells the display where to find the updated pixels. This process is called flipping.
Applications can access device data with regular file functions, but the devices themselves are controlled with a special function called the input/output control, or ioctl
. When the kernel receives a ioctl
, it directs the request to the driver of the device being accessed. The ioctl
function is simple: it either modifies a device property with user data or reads data from the device.
As declared in sys/ioctl.h, ioctl
accepts three parameters:
An int
file descriptor representing the open device
An int
request code identifying the device property being read or written to
The device-specific data to be read from or written to the device
The third argument is usually an int
or a void*
, but its precise datatype is determined by the device type. The ioctl
function returns an int
corresponding to the function’s completion status.
This section explains how to control the frame buffer with ioctl
s, particularly those needed for double buffering. It presents the ioctl
codes provided by the Linux operating system first and then describes those provided specifically for the Sony PlayStation 3.
If you look through the linux/fb.h header file, you’ll see hundreds of constants related to the Linux frame buffer. Some are ioctl
request codes, and others are bit masks that clarify the meaning of ioctl
’s output data. Table 19.1 presents ten of the most important request codes available for all Linux frame buffers.
Table 19.1. Important Linux Frame Buffer ioctl
Request Codes
Name of Request Code | R/W | Purpose |
---|---|---|
| R | Returns status of current scan |
| R | Read frame buffer variable dimensions |
| W | Write frame buffer variable dimensions |
| R | Read frame buffer fixed dimensions |
| R | Read frame buffer color map (palette) |
| W | Frame buffer color map (palette) |
| W | Enable image panning |
| W | Clears the display |
| W | Allocate memory on frame buffer |
| W | Free allocated memory |
The first code, FBIOGET_VBLANK
, is particularly important because it tells us about the system’s scan operations. If ioctl
is called with this request code, it will produce an fb_vblank
structure composed of the following fields:
unsigned int flags
: Identify scan status (see the following paragraph)
unsigned int count
: Number of traces since startup
unsigned int vcount
: Current vertical position
unsigned int hcount
: Current horizontal position
unsigned int reserved[4]
: Reserved for future capability
The flags
field provides information about the capabilities supported by the current system. To interpret the information in this field, you have to compare it to bit masks declared in linux/fb.h. The bit mask that concerns us is FB_VBLANK_HAVE_VSYNC
. If a bitwise AND between this field and the flags
data is high, applications will be able receive vertical sync signals.
For example, the following code calls ioctl
with the FBIOGET_VBLANK
code and analyzes the result to determine whether vertical sync signals can be detected:
struct fb_vblank ret_data; /* Get information about the frame buffer */ ioctl(fd, FBIOGET_VBLANK, ret_data); /* Check if the application can receive vsync signals */ if (ret_data & FB_VBLANK_HAVE_VSYNC) vertical_sync_signals_can_be_detected(); else vertical_sync_signals_cannot_be_detected();
Vertical sync signals control when frame buffers can be flipped, and are therefore very important in multibuffered graphic applications. But before you rely on the signals, it’s a good idea to check whether they can be detected.
In addition to the Linux ioctl
codes, developers can access the PlayStation 3 frame buffer with a custom set of request codes. Sony has provided these freely and they are all declared in asm/ps3fb.h. Table 19.2 lists the seven new ioctl
codes, along with whether they read or write and their purpose.
Table 19.2. PS3 Frame Buffer ioctl
Request Codes
Name of Request Code | R/W | Purpose |
---|---|---|
| R | Halt processing until a vertical sync signal is received |
| W | Transfer frame buffer data to display and submit flip request |
| R | Obtain information about the frame buffer environment |
| W | Allow the frame buffer to be controlled by user applications |
| W | Discontinue application access to the frame buffer |
| R | Read video mode of PlayStation 3 console |
| W | Set video mode of PlayStation 3 console |
FBIO_WAITFORVSYNC
looks like a Linux request code, but it’s a PS3-specific code that forces ioctl
to block until a vertical sync signal is received. When the sync signal appears, the application can continue processing. When the application finishes computing pixel data for the frame buffer, it calls ioctl
with the FBIO_IOCTL_FSEL
request code. This tells the hypervisor that new data is ready to be displayed and provides the address where the data can be located.
The third request code, PS3FB_IOCTL_SCREENINFO
, provides information about the frame buffer and display dimensions. With this code, ioctl
provides return data in a struct
called ps3fb_ioctl_res
whose fields are given by:
Figure 19.1 shows how xres
, yres
, xoff
, and yoff
relate to the dimensions of a frame. Listing 19.1 shows how these parameters can be acquired in code using ioctl
and PS3FB_IOCTL_SCREENINFO
. This application calls this function to acquire the screen data and prints the results.
Example 19.1. Checking Display Parameters: ppu_fbcheck.c
#include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/fb.h> #include <asm/ps3fb.h> int main() { int fd; struct ps3fb_ioctl_res info; /* Open the framebuffer device file */ if ((fd = open("/dev/fb0", O_RDWR)) < 0) { fprintf(stderr, "error open:%d ", fd); return -1; } /* Acquire screen information */ if (ioctl(fd, PS3FB_IOCTL_SCREENINFO, (unsigned long)&info) < 0) { fprintf(stderr, "PS3FB_IOCTL_SCREENINFO failed "); return -1; } /* Display the results */ printf("The total x dimension is %d ", info.xres); printf("The total y dimension is %d ", info.yres); printf("The x margin is %d ", info.xoff); printf("The y margin is %d ", info.yoff); printf("The number of framebuffers is %d ", info.num_frames); return 0; }
The next two request codes in the table, PS3FB_IOCTL_ON
and PS3FB_IOCTL_OFF
, tell the system whether the frame buffer is controlled by the application or the operating system. When the application needs exclusive access to the frame buffer, it should call ioctl
with PS3FB_IOCTL_ON
. When the application is finished using the frame buffer, it should call PS3FB_IOCTL_OFF
.
The last two codes, PS3FB_IOCTL_GETMODE
and PS3FB_IOCTL_SETMODE
, control the video settings for the display. This is a complicated subject and lies beyond the scope of this chapter. But if you need to view or change your video settings, I strongly recommend using the ps3videomode
command rather than ioctl
s. To see how this works, enter ps3videomode -v
at the Linux command line.
Now that you’ve seen how ioctl
s access the frame buffer, it’s time to start drawing pixels. The process is easy if you know how pixels and pixel colors are stored in the frame buffer. The following four points are assumed:
Each pixel occupies 32 bits.
The red component occupies the second byte (Bits 8–15).
The green component occupies the third byte (Bits 16–23).
The blue component occupies the least significant byte (Bits 24–31).
Figure 19.2 shows what this looks like in memory.
The process of configuring pixels in the frame buffer is simple. Each red, green, and blue component is set to a value between 0 and 255. The green value is shifted left by 8 bits, and the red value is shifted left by 16 bits. The three components are ORed together to produce a complete pixel value. Once the pixel’s value is set, the next pixel in the row is modified, and the process continues for every pixel in the frame.
Listing 19.2 shows how this is implemented in code. The draw
function sets an unsigned int*
equal to the start of the memory-mapped frame buffer and then iterates through each pixel in the frame. If the distance between the pixel coordinate and the center of the frame buffer is less than RADIUS
, the application defines the pixel’s color.
Example 19.2. Drawing to the Frame Buffer, Single Buffered: ppu_fbsingle.c
#include <stdio.h> #include <fcntl.h> #include <math.h> #include <unistd.h> #include <string.h> #include <sys/ioctl.h> #include <linux/fb.h> #include <asm/ps3fb.h> #include <sys/mman.h> #define BYTES_PER_PIXEL 4 #define RADIUS 20 void draw(int fd, void *fb_addr, struct ps3fb_ioctl_res *info) { int i, x, y, x_size, y_size; unsigned int red, green, blue; unsigned int *pix_addr; unsigned int frame = 0; /* Number of rows to be drawn */ y_size = info->yres - 2 * info->yoff; /* Number of columns to be drawn */ x_size = info->xres - 2 * info->xoff; /* Repeat drawing process 128 times */ for (i=0; i<128; i++) { /* Wait for vertical sync signal */ ioctl(fd, FBIO_WAITFORVSYNC, 0); /* Iterate for each row */ for (y=0; y<y_size; y++) { // Set pixel address to start of row pix_addr = (unsigned int*) (fb_addr + y * info->xres * BYTES_PER_PIXEL); for (x=0; x<x_size; x++) { if (sqrt(pow(x-x_size/2,2)+pow(y-y_size/2,2)) < RADIUS+i) { red = 0xff; green = 0xa5; blue = i*2; } else { red = green = blue = 0; } *pix_addr = (red << 16 | green << 8 | blue); pix_addr++; } } /* Identify buffer for the next flip */ ioctl(fd, PS3FB_IOCTL_FSEL, (unsigned long)&frame); } } int main() { void *addr; struct ps3fb_ioctl_res info; struct fb_vblank vblank; int fd, disp_size, margin_size, fb_size; /* Open the frame buffer device file */ if ((fd = open("/dev/fb0", O_RDWR)) < 0) { fprintf(stderr, "error open:%d ", fd); return -1; } /* Make sure the vsync signal is accessible */ ioctl(fd, FBIOGET_VBLANK, (unsigned long)&vblank); if (!(vblank.flags & FB_VBLANK_HAVE_VSYNC)) { fprintf(stderr, "Error accessing vertical sync "); close(fd); return -1; } /* Acquire information about display/framebuffer */ if (ioctl(fd, PS3FB_IOCTL_SCREENINFO, (unsigned long)&info) < 0) { fprintf(stderr, "Error accessing screen info "); close(fd); return -1; } /* Determine buffer size and memory map buffer to addr */ disp_size = info.xres * info.yres * BYTES_PER_PIXEL; margin_size = info.xres * info.yoff * 2 * BYTES_PER_PIXEL; fb_size = disp_size - margin_size; addr = mmap(NULL, fb_size, PROT_WRITE, MAP_SHARED, fd, 0); if (addr == MAP_FAILED) { fprintf(stderr, "Error mapping framebuffer to memory "); close(fd); return -1; } /* Turn on fb access, draw pixels, turn off */ ioctl(fd, PS3FB_IOCTL_ON, 0); memset(addr, 0, fb_size); draw(fd, addr, &info); ioctl(fd, PS3FB_IOCTL_OFF, 0); munmap(NULL, fb_size); close(fd); return 0; }
This application determines the frame buffer’s location and size and then uses this information to draw 128 circles of varying color. It waits for the vertical sync signal and then fills the frame buffer. When every pixel value is set, the application tells the display that the data is ready and waits for another vertical sync signal.
The ppu_fbsingle
application sets pixels within a single buffer. The ppu_fbmulti
application accomplishes the same result, but uses all the available frame space in the PS3 frame buffer. That is, it reads the ps3fb_ioctl_res
object to determine the number of frames that can be stored in the frame buffer (five on my PS3) and maps the buffer to user space.
After ppu_fbmulti
maps the frame buffer memory, each call to draw
accesses a different frame from the one before it. This is accomplished with the following code:
/* Set the frame number */ frame = i % num_frames; /* Determine the address of the current frame */ frame_addr = fb_addr + (info->xres * info->yres * BYTES_PER_PIXEL) * frame;
The rest of the loop draws pixels in the memory region starting at frame_addr
. When this is completed, the application identifies the drawing region with the following line:
ioctl(fd, PS3FB_IOCTL_FSEL, (unsigned long)&frame);
ppu_fbsingle
and ppu_fbmulti
take longer to run than you might expect for applications running on a third-generation console like the PlayStation 3. This is because the PPU doesn’t have the computational power to perform the pixel calculations quickly. The situation would be improved if the RSX graphics accelerator was accessible, but we can do better still by harnessing the SPUs.
Low-level graphics development isn’t difficult, but it takes time to understand the basics: display synchronization, frame buffers, Linux devices, and ioctl
s. Documentation on these topics is sparse and vague, but this chapter should suffice as an introduction.
The /dev/fb0 device file represents the Linux frame buffer. It can be opened, memory mapped, and closed like any other file. But because it’s a block file, all reads and writes must be buffered. The /dev/fb0 buffer, called the frame buffer, can be accessed and controlled with the I/O Control (ioctl
) command. An ioctl
accepts a file descriptor, a request code, and a data pointer, and either reads data from the device or writes data to it.
The example applications in this chapter are geared toward the PlayStation 3 because of the additional ioctl
request codes provided by Sony. With these new codes, applications can wait until the vertical sync signal arrives before computing data for the frame buffer. Further, with the PS3FB_IOCTL_FSEL
code, an application can tell the PS3 hypervisor that data is ready and provide a flip request.
When you understand how to access the frame buffer, it’s easy to modify its pixel data. You just find the dimensions of the displayed pixels and loop through each address. Colors are assigned to pixels according to their coordinates, and if multiple frames are available inside a buffer, data can be flipped in and out for smoother display.
As rewarding as low-level development can be, you wouldn’t want to create 3D graphics by setting the pixels in a frame buffer. It’s easier and more time-efficient to code graphics with a high-level language that automatically handles low-level tasks. The next chapter presents one of the most popular of these languages: OpenGL.
3.142.201.206