Chapter 19. Programming the Frame Buffer: Linux and the PlayStation 3

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.

Note

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.

Graphical Displays, Linux Devices, and the Frame Buffer

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.

Display Monitors and Linux Configuration

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.

A graphic display

Figure 19.1. A graphic display

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.

Display Speed and Frame Rate

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.

Linux Devices and 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:

  1. Device files allow for underlying device access through the I/O control (ioctl) function.

  2. 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.

Character 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

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.

I/O Control (ioctl) Instructions

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:

  1. An int file descriptor representing the open device

  2. An int request code identifying the device property being read or written to

  3. 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 ioctls, 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.

Linux Frame Buffer I/O Control

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

FBIOGET_VBLANK

R

Returns status of current scan

FBIOGET_VSCREENINFO

R

Read frame buffer variable dimensions

FBIOPUT_VSCREENINFO

W

Write frame buffer variable dimensions

FBIOGET_FSCREENINFO

R

Read frame buffer fixed dimensions

FBIOGETCMAP

R

Read frame buffer color map (palette)

FBIOPUTCMAP

W

Frame buffer color map (palette)

FBIOPAN_DISPLAY

W

Enable image panning

FBIOBLANK

W

Clears the display

FBIO_ALLOC

W

Allocate memory on frame buffer

FBIO_FREE

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.

PlayStation 3 Frame Buffer I/O Control

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

FBIO_WAITFORVSYNC

R

Halt processing until a vertical sync signal is received

PS3FB_IOCTL_FSEL

W

Transfer frame buffer data to display and submit flip request

PS3FB_IOCTL_SCREENINFO

R

Obtain information about the frame buffer environment

PS3FB_IOCTL_ON

W

Allow the frame buffer to be controlled by user applications

PS3FB_IOCTL_OFF

W

Discontinue application access to the frame buffer

PS3FB_IOCTL_GETMODE

R

Read video mode of PlayStation 3 console

PS3FB_IOCTL_SETMODE

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:

  • unsigned int xres: x-dimension of the frame buffer

  • unsigned int yres: y-dimension of the frame buffer

  • unsigned int xoff: x-margin of the frame buffer

  • unsigned int yoff: y-margin of the frame buffer

  • unsigned int num_frames: Number of frames stored in the frame buffer

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 ioctls. To see how this works, enter ps3videomode -v at the Linux command line.

Drawing the Frame Buffer

Now that you’ve seen how ioctls 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:

  1. Each pixel occupies 32 bits.

  2. The red component occupies the second byte (Bits 8–15).

  3. The green component occupies the third byte (Bits 16–23).

  4. The blue component occupies the least significant byte (Bits 24–31).

Figure 19.2 shows what this looks like in memory.

Pixels and their RGB color components

Figure 19.2. Pixels and their RGB color components

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.

Conclusion

Low-level graphics development isn’t difficult, but it takes time to understand the basics: display synchronization, frame buffers, Linux devices, and ioctls. 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.168.83