Home Memoirs of a Gamer Movies I watched Guidebook Links
There is a considerable time span in the DOS era during which EGA 16 color graphics were considered if not the state of the art, then at least the defacto standard.
Assembly | Borland Turbo C++ / Open Watcom 16bit |
Open Watcom 32bit |
xor ah, ah mov al, 0x0D int 0x10 |
union REGS regs; regs.h.ah = 0; regs.h.al = 0x0D; int86(0x10, ®s, ®s); |
union REGS regs; regs.h.ah = 0; regs.h.al = 0x0D; int386(0x10, ®s, ®s); |
EGA is a bitmap mode, similar to most graphics modes available to IBM PC compatibles. This means drawing to the screen is as simple as taking the video ram's base address 0xA0000 and adding an offset calculated by multiplying the Y position by the screen's width in bytes and adding the X coordinate.
Thus drawing a dot in the top left hand corner of the screen is as easy as setting the first byte of vram (0xA0000) to 1! Hey presto, we're drawing!
Assembly | Borland Turbo C++ | Open Watcom 32bit |
mov ax, 0xA000 mov ds, ax xor bx, bx mov [ds:bx], byte 0x01 |
unsigned char far *screen; screen = MK_FP(0xA000,0); screen[0] = 1; |
unsigned char *screen; screen = (void*)0xA0000; screen[0] = 1; |
Well...almost. You see, there's something to bare in mind with EGA...
The Bit Planes
Now, there is a caveat to drawing in EGA that isn't generally a problem in most other video modes. EGA uses a memory model called bit planes.
What are bit planes, you ask? To put simply instead of packing multiple pixels into a single byte of data like CGA and Tandy 16 color modes do, EGA decided to implement what is functionally 4 monochrome vram buffers. Every 8th pixel all 4 of those vram banks load the next byte's worth of pixels into a shift register. 8 bits go in and one bit goes out. Each pixel clock tick causes the shift register to output the next bit, just like in Monochrome graphics modes. Why 4 planes? Well because if you put 4 monochrome video buffers together, you get 4 bits per pixel, or 16 colors. Sounds simple, right? From a hardware perspective, it is!
Unfortunately we're not writing hardware, we're writing software, and that hardware simplification requires we do a little extra in software.
So how are these bit planes combined? Well on original CGA displays, to facilitate 16 color text mode, there was a TTL signal for Blue, Green, Red, and Intensity.
What intensity did on a hardware level was push the 3 color signals up by 1/3rd. Sort of anyways. Brown is a special case, since strictly following that model gives you a more olive color rather than brown. Coming from the 70's I guess everyone had their fill of yellowy olive colors and wanted something a bit more woody colored. I could name other brown materials here but let's keep this PG.
Each bit plane can be activated for writing independently of the others. This is why when you wrote 1 to vram above you got a white pixel. By default all 4 bit planes are enabled for writing. This means you can write black and white graphics really easily! Writing a single byte can set all 4 bit planes at once, making the color bright white! We can set any combination of the bit planes to active too! If we want to draw something that is Brown and Black, we can turn on the Red and Green channels and write a single byte. This also means we can draw completely different images to each bit plane and the hardware will blend the buffers together for us for nifty affects!
So, how do we control which bit planes we're writing to anyways? Well I'm glad you asked...
There are two important registers in the EGA spec: 0x03C4 and 0x03C5. The actual functionality of these two registers is pretty complicated and go far beyond the scope of this chapter, but a reference to the OSDev website can be found at the bottom of this page for full details. For our needs, we just need to know 0x03C4 is an index value and it needs to be set to 2, and then 0x03C5 gets the value for the register indexed in 0x03C4 which needs to be set to the desired bit mask.
Assembly | Borland Turbo C++ / Open Watcom |
mov dx, 0x03C4 mov al, 0x02 out dx, al inc dx mov al, mask out dx, al |
outp(0x03C4,0x02); outp(0x03C5,mask); |
The value stored in mask is going to depend on which planes we want to write to.
Mask Value (binary) | Plane |
1 (0001) | Blue |
2 (0010) | Green |
4 (0100) | Red |
8 (1000) | Intensity |
To combine masks just or the planes you want together:
Assembly | Borland Turbo C++ / Open Watcom |
mov al, 0x01 or al, 0x02 or al, 0x08 |
mask = 0x01; mask = mask | 0x02; mask |= 0x08; |
VGA, being 100% backwards compatible with CGA and EGA as part of the spec introduces a fun little quirk. See, VGA cables don't take TTL signals for Blue, Green, and Red. Instead they have analog voltage levels controlling the color. The closer to Ground, the darker the color, the closer to 0.7 Volts, the brighter. So unlike CGA and EGA cards which are physically wiring signals to each of the 4 "color" planes, VGA cards send all requests to a 256 color palette register. This means that at least in EGA modes, most cards will allow a program to replace the standard EGA palette with a customized VGA palette with 18 bits of color depth per color value! Then just combine the bit planes such that the Blue channel is our 1st bit, Green is our 2nd, Red our 3rd, and Intensity becomes our 4th bit. 4 bits means 16 colors, right? So for each pixel we can combine our bit planes to make color indices 0 to 15.
There's just one catch....
VGA cards often don't map EGA colors to palette indices 0 through 16. In my experience it's best to start with something like:
EGA Color Index | VGA Color Index |
0 | 0 |
1 | 1 |
2 | 2 |
3 | 3 |
4 | 4 |
5 | 5 |
6 | |
7 | 7 |
8 | 16 |
9 | 17 |
10 | 18 |
11 | 19 |
12 | 20 |
13 | 21 |
14 | 22 |
15 | 23 |
Then explore from there. I should also warn you that just because your palette mapping works on one VGA card does not mean it will necessarily work on all, so test on as many different platforms as possible to ensure you find all edge cases.
Just like with initialization, exiting EGA graphics mode is super simple!
Assembly | Borland Turbo C++ / Open Watcom 16bit |
Open Watcom 32bit |
xor ah, ah mov al, 0x03 int 0x10 |
union REGS regs; regs.h.ah = 0; regs.h.al = 0x03; int86(0x10, ®s, ®s); |
union REGS regs; regs.h.ah = 0; regs.h.al = 0x03; int386(0x10, ®s, ®s); |