Drawing In a Linear Framebuffer

From OSDev Wiki
Jump to navigation Jump to search

Now that you know how you can easily write text to the screen using Hardware VGA support, you might be wondering how you'll be able to display nice images, windows, menus, icons, fancy cursors and buttons, etc. This page describes how to display graphics in a linear framebuffer, a simple array mapped in memory that represents the screen.

Graphics Modes

Main article: Getting VBE Mode Info
Main article: GOP

VGA and VBE modes can be selected using BIOS interrupt 0x10 (while in real mode). INT 0x10, VESA Video Modes and VESA are resources for, VGA and VBE/VESA.

VGA is limited to 16-color 640x480, meanwhile VBE (BIOS systems) and GOP (UEFI machines excluding some very early ones) can go up to the monitor and video card's maximum supported resolution.

Switching

The cleanest way to set up your video mode is to go through the video BIOS. It can be performed through the regular Int 0x10 interface, or through the (optional) Protected mode interface offered by VBE3. As you can guess, Int 0x10 requires a 16-bit environment, so you can only use it in Real Mode or Virtual 8086 Mode

Practically, the options are (in order of difficulty):

  • Set up the mode you want at early stage (in the bootloader) before entering protected mode.
    • Develop on an UEFI system, and get a GOP framebuffer at boot-time.
    • Let your Bootloader do the switch for you.
  • Switch back to Real Mode or Unreal Mode for setting the proper video mode (Napalm at rohitab.com has a neat little function for reference.)
  • Write a VGA driver that can do low-resolution modes on practically all hardware
  • Use the PMID from VBE3, if present
  • Set up a V8086 monitor that will execute the mode-switching code
  • Run a software code translation tool to produce pmode code out of bios rmode code. (SANiK is on the catch)
  • You write a driver for your specific graphics card

Locating Video Memory

For standard VGA video modes the video memory will be at address 0xA0000 for EGA/VGA video modes and 0xB8000 for CGA and text modes. To find out which one look at the following table:

00 text 40*25 16 color (mono)
01 text 40*25 16 color
02 text 80*25 16 color (mono)
03 text 80*25 16 color
04 CGA 320*200 4 color
05 CGA 320*200 4 color (m)
06 CGA 640*200 2 color
07 MDA monochrome text 80*25
08 PCjr
09 PCjr
0A PCjr
0B reserved
0C reserved
0D EGA 320*200 16 color
0E EGA 640*200 16 color
0F EGA 640*350 mono
10 EGA 640*350 16 color
11 VGA 640*480 mono
12 VGA 640*480 16 color
13 VGA 320*200 256 color

For VESA modes, the framebuffer address is stored in the mode info block. This is the physical address of the linear framebuffer (it is not a 16-bit far pointer but a 32-bit linear pointer) : if you use paging, you have to map it somewhere to use it. The length of the framebuffer in bytes is pitch * height.

For GOP, the framebuffer address is in the EFI_GRAPHICS_PROTOCOL struct, gop->Mode->FrameBufferBase. GOP does not support teletype character modes, only graphics pixel oriented modes.

Plotting Pixels

Location

If you wanted to plot a red pixel in the middle of your screen. The first thing you have to know is where the middle of the screen is. In 320x200x8 (mode 13), this will be at 100x320+160 = 32160. In general, your screen can be described by:

width how many pixels you have on a horizontal line
height how many horizontal lines of pixels are present
pitch how many bytes of VRAM you should skip to go one pixel down
depth how many bits of color you have
"pixelwidth" how many bytes of VRAM you should skip to go one pixel right.

"pitch" and "width" may seem redundant at first sight but they aren't. It's not rare once you go to higher (and exotic) resolutions to have e.g. 8K bytes per line while your screen is actually 1500 pixels wide (32-bits per pixel). The good news is that it allows smooth horizontal scrolling (which is mainly useful for 2D games :P )

Pitch and pixel width are usually announced by VESA mode info. Once you know them, you can calculate the place where you plot your pixel as:

unsigned char *pixel = vram + y*pitch + x*pixelwidth;

Color

The second thing to know is what value you should write for "red". This depends on your screen setup, again. In EGA mode, you have a fixed palette featuring dark-red (color 4) and light-red (color 12). Yet, EGA requires you to plot each bit of that on different pixel plane, so refer to EGA programming tutorials if you really want such modes supported. In conventional 320x200x8 VGA mode, you have the same colours 4 and 12 as in EGA so you would plot your red pixel with

*pixel = 4;

Yet, in VGA, the palette is reprogrammable (as you can learn in FreeVGA documents), so virtually any value between 0..255 could be 'red' if you program the palette so :P

So a full "putpixel" function for VGA would be

/* example for 320x200 VGA */
void putpixel(int pos_x, int pos_y, unsigned char VGA_COLOR)
{
    unsigned char* location = (unsigned char*)0xA0000 + 320 * pos_y + pos_x;
    *location = VGA_COLOR;
}

Finally, in VESA and GOP modes, you usually have truecolor or hicolor, and in both of them, you have to give independent red, green and blue values for each pixel. modeinfo will (again) instruct you of how the RGB components are organized in the pixel bits. E.g. you will have 0xRRRRRGGGGGBBBBB for 15-bits mode, meaning that #ff0000 red is there 0x7800, and #808080 grey is 0x4210 (pickup pencil, draw the bits and see by yourself)

static void putpixel(unsigned char* screen, int x,int y, int color) {
    unsigned where = x*pixelwidth + y*pitch;
    screen[where] = color & 255;              // BLUE
    screen[where + 1] = (color >> 8) & 255;   // GREEN
    screen[where + 2] = (color >> 16) & 255;  // RED
}

Optimizations

It can be tempting from here to write other drawing functions from calls to putpixel... Don't. Drawing a filled rectangle means you access successive pixels and then advance by "pitch - rect_width" to fill the next line. If you do a "for(y=100;y<200;y++) for(x=100;x<200;x++) putpixel (screen,x,y,RED);" loop, you'll recompute 'where' about 10,000 times, as well as make an excessive amount of function calls. Even if the compiler has optimized y*pitch into adds and shifts instead of multiplication, it's silly to waste CPU time while you could do

static void fillrect(unsigned char *vram, unsigned char r, unsigned char g, unsigned   char b, unsigned char w, unsigned char h) {
    unsigned char *where = vram;
    int i, j;

    for (i = 0; i < w; i++) {
        for (j = 0; j < h; j++) {
            //putpixel(vram, 64 + j, 64 + i, (r << 16) + (g << 8) + b);
            where[j*pixelwidth] = r;
            where[j*pixelwidth + 1] = g;
            where[j*pixelwidth + 2] = b;
        }
        where+=pitch;
    }
}

That should be enough to get you started coding (or googling for) a decent video library.

Drawing Text

Main article: VGA Fonts

Once in graphic mode, you no longer have the BIOS or the hardware to draw fonts for you. The basic idea is to have font data for each character and use it to plot (or not to plot) pixels. There are plenty of ways to store those fonts depending on whether they have multiple colors or not, alpha channel or not etc. What you will basically have, however, is:

// holding what you need for every character of the set
font_char* font_data[CHARS];
 
// rendering one of the character, given its font_data
void draw_char(screen, where, font_char*);

void draw_string(screen, where, char* input) {
    while(*input) {
        draw_char(screen,where,font_data[input]);
        where += char_width;
        input++;
    }
}
 
void draw_char(screen, where, font_char*) {
    for (l = 0; l < 8; l++) {
        for (i = 8; i > 0; i--) {
            j++;
            if ((font_char[l] & (1 << i))) {
                c = c1;
                put_pixel(j, h, c);
            }
        }
        h++;
        j = x;
    }
}

Drawing Icons

Main article: Loading Icons

For a GUI, you'll probably want to display icons. It's as simple as

void draw_icon(x, y, w, h, pixels) {
    for (l = j = 0; l < h; l++) {
        for (i = 0; i < w; i++, j++) {
            put_pixel(x + i, y + l, pixels[j]);
        }
    }
}

The difficulty is, icons aren't stored as width, height and pixels array on disk. First you have to decode an image file to get that information.

See Also

External Links