## 15 January, 2011

### CGA programming in C

CGA (Color Graphics Adapter) is an ancient graphical adapter from IBM introduced in 1981. It was IBM's first color graphical card and supported several graphical modes. I will describe programming in 640x200 black/white (monochrome) mode.

Each pixel can be accessed independently (but you still can address just bytes, so each time you can modify 8 pixels).
All lines divided into even and odd. Memory of even lines located at 0xB8000. 80 bytes used for each line, thus you need 8000 bytes for each bank of lines.
Because of performance extra 192 bytes added to each bank (8192 is a power of 2), so odd lines start at 0xBA000.
The code below used to put/clear pixel from the sceen:
#define EVEN_LINES 0xB8000
#define ODD_LINES 0xBA000

unsigned char *mem[] = {(unsigned char*) EVEN_LINES,
(unsigned char*) ODD_LINES};

static void drv_put_pixel(int x, int y, unsigned int color)
{
unsigned char mask = 0x80 >> (x % 8);
int offset = y/2 * 640/8 + x/8;
if (color)
*(mem[y % 2] + offset) |= mask;
else
*(mem[y % 2] + offset) &= ~mask;
}

Before you put any pixel you should activate this video mode (BIOS interrupt 10 to set video mode 6):
int video_init()
{
__asm__("\
mov $0, %ah;\ mov$6, %al;\
int \$0x10;\
");
}

In this graphical mode pixel aspect ratio of 1:2.4, so if you just plot pixels as you always do with any high level drawing library you might get following picture:

To fix this issue you can use framebuffer to plot pixels in application's memory:
#define FB_LENGTH       (SCREEN_WIDTH*SCREEN_HEIGHT)
#define DRAW_PIXEL(x, y, colour) frame_buffer[(y)*SCREEN_WIDTH + (x)] =\
(colour)
color_t frame_buffer[FB_LENGTH];


Then use following code to copy from framebuffer to video memory:
void fb_to_screen() {
int x, y, Yfb;

for (y = 0, Yfb = 0; Yfb < SCREEN_HEIGHT; y++, Yfb += 2) {
for (x = 0; x < SCREEN_WIDTH; x++) {
color_t pixel1 = frame_buffer[Yfb * SCREEN_WIDTH + x];
if (Yfb != 0) {
color_t pixel0 = frame_buffer[(Yfb - 1) * SCREEN_WIDTH + x];
color_t pixel2 = frame_buffer[(Yfb + 1) * SCREEN_WIDTH + x];
if (pixel0 == pixel2 && pixel0 != pixel1) {
drv_put_pixel(x, y, pixel1);
} else {
/* "Merge" line1 and line2 */
drv_put_pixel(x, y, pixel2);
}
} else {
drv_put_pixel(x, y, pixel1);
}
}
}
}

It might be a good idea to optimize routine above, e.g. to sync only modified sections of the screen (or at least just modified lines). Now the picture looks fine (for algorithm you may refer to the code). and also you got some extra drawing space (not shown on the picture) because of scaling:
The problem with CGA programming is that int 10h is forbidden in modern operating systems (like access to the video memory). AFAIK it can be done in MS-DOS only (that nifty real mode applications), thus you need to compile the code into 16-byte dos application and run it under dos, or in windows dos compatible mode or in DOSBox (recommended). Following excellent VGA-programming tutorial is based on DJGPP 2.0 or Borland C, Turbo C. I failed to execute in Windows XP their binary produced by DJPP, but it worked fine in DOSBox with csdpmi5b unpacked to the app directory. In next article I will shortly describe running 32-bit windows applications in DOS. If you write in ASM you can use tasm and probably masm. To learn how to draw primiteves I recommend David Brackeen's "256-Color VGA Programming in C" mentined earlier.

smeezekitty said...

I was able to successfully program for a CGA adapter in Linux with LRMI.

Evgeniy Ivanov said...

@smeezekitty