VOGONS


Pulling my hair out with EGA programming.

Topic actions

First post, by keenmaster486

User metadata
Rank l33t
Rank
l33t

I can set the video mode. I can set the border color using the EGA registers.

Writing 1 to plane 0 on the first pixel to turn it blue, however, seems to be impossible for me, despite it being unfathomably straightforward.

outp(0x3C4, 0x02); // select plane-select register
outp(0x3C5, 0x01); // plane 0
unsigned char* p = (unsigned char*) 0xA000;
*p = 0x01;

What on earth is going wrong here? I have tried multiple different methods of doing this.

This page: http://www.techhelpmanual.com/89-video_memory_layouts.html has a slightly different method. It doesn't work either.

Just testing this in DOSBox.

Would appreciate some help. I have been trying to make this work for 2 days. I'm sure there is something really, really stupid I'm doing wrong.

World's foremost 486 enjoyer.

Reply 1 of 24, by pan069

User metadata
Rank Oldbie
Rank
Oldbie

Edit: I think you're missing a couple of zeros on your video memory address. It should be 0xa0000000L not 0xa000.

Just doing this in assembler, following the tutorial you pointed out, I get a blue pixel:

  mov  ax,0dh
int 10h

mov dx,3ceh
mov ax,0005h
out dx,ax

mov dx,3c4h
mov ax,0102h
out dx,ax

mov ax,0a000h
mov es,ax
xor di,di

mov al,80h
stosb

Just keep in mind that using "1" represents the least significant bit in the byte, so to get the first pixel you should use 0x80, i.e. the most significant bit.

Edit: from looking at your code, you might have gotten your ports mixed up, i.e. you're using 3c4 & 3c5 instead of 3ce & 3c4.

Reply 2 of 24, by HanSolo

User metadata
Rank Member
Rank
Member

You want to write to address 0xa0000, not 0xa000. In real mode you need to split that into segment 0xa000 and offset 0. In Assembler that is done like pano69 said. I'm not 100% sure about real mode C but I think you have to construct it as a far pointer which combines segment and offset and the compiler handles the rest:

unsigned char far *ptr = (unsigned char far*) 0xa0000000;

This notation is somewhat misleading because the actual address is not 0xa0000000 but segment (0xa000) + offset (0x0000), i.e. 0xa000 * 16 + 0x0000

Reply 4 of 24, by keenmaster486

User metadata
Rank l33t
Rank
l33t

Thanks, guys. Using a far pointer worked. I can write pixels now.

HanSolo wrote on 2023-05-28, 10:32:

You want to write to address 0xa0000, not 0xa000. In real mode you need to split that into segment 0xa000 and offset 0. In Assembler that is done like pano69 said.

I'm not exactly sure what's going on in that assembler code. What is the purpose of xor di, di?

HanSolo wrote on 2023-05-28, 10:32:
...I think you have to construct it as a far pointer which combines segment and offset and the compiler handles the rest: […]
Show full quote

...I think you have to construct it as a far pointer which combines segment and offset and the compiler handles the rest:

unsigned char far *ptr = (unsigned char far*) 0xa0000000;

This notation is somewhat misleading because the actual address is not 0xa0000000 but segment (0xa000) + offset (0x0000), i.e. 0xa000 * 16 + 0x0000

This works, but I'm not exactly sure what the compiler is actually doing there.

llm wrote on 2023-05-28, 11:01:

This is really useful. Thank you.

World's foremost 486 enjoyer.

Reply 5 of 24, by Scali

User metadata
Rank l33t
Rank
l33t
keenmaster486 wrote on 2023-05-28, 21:30:

I'm not exactly sure what's going on in that assembler code. What is the purpose of xor di, di?

XOR stands for Exclusive-Or.
It's a bitwise operator.
This is a simple trick: when you bitwise-xor any value with itself, the result is always 0.
It's a slightly fancier version of sub di, di (subtracting any value from itself also yields 0).
For some reason the xor-version became the default way of zeroing registers on x86. On other platforms, the sub-version became the default way.
Technically there's no reason to choose one over the other.

In this case, di is set to 0, because of the stosb register: stosb stores the contents of the al register to location es:[di], and then increments di by 1.
So if you set es to the VRAM segment and di to 0, you point to the start of video memory, and can then start filling it with stosb.

http://scalibq.wordpress.com/just-keeping-it- … ro-programming/

Reply 6 of 24, by HanSolo

User metadata
Rank Member
Rank
Member
keenmaster486 wrote on 2023-05-28, 21:30:

I'm not exactly sure what's going on in that assembler code. What is the purpose of xor di, di?

The assembler code for the actual pixel setting above can be rewritten as follows which might be easier to understand:

mov ax,0a000h
mov es,ax
mov di,0
mov al,80h
mov es:[di],al

The last line is then equivalent to "mov 0xa000:0000,0x80" (which is not possible in this direct form)

This works, but I'm not exactly sure what the compiler is actually doing there.

The compiler will be doing more or less the same, since there aren't many ways this can be done. The essential part (for all real-mode programming) is to understand the usage of segment and offset

Reply 7 of 24, by jakethompson1

User metadata
Rank Oldbie
Rank
Oldbie

You might consider MK_FP(0xa000, 0); instead of 0xa0000000 to keep the code even clearer.

Reply 8 of 24, by keenmaster486

User metadata
Rank l33t
Rank
l33t

Thank you all for your help so far. Very helpful.

Do you guys think I can improve these functions? This feels very brute force-y, like there must be some optimization I can do. The pixel setting is not very fast, although of course I will have to write a blit function for actual graphics drawing. I anticipate having to use the pixel function for particle effects, stuff like that.

void egaSelectPlane(unsigned char p) {
// p can have one of four values to select a single plane:
// 0x01 - plane 0
// 0x02 - plane 1
// 0x04 - plane 2
// 0x08 - plane 3

// Or any aggregation of those to select multiple planes
// Example:
// 0x0F - all planes

outp(0x3C4, 0x02); // select plane-select register
outp(0x3C5, p);
}

unsigned char egaGetPlanesForColor(unsigned char c) {
// gets a composite value for the planes you should write to for a particular color
// bitplanes: BGRI (0,1,2,3)
// plane 0 - blue
// plane 1 - green
// plane 2 - red
// plane 3 - intensity

// 0x00 color 0 - black
// 0x01 color 1 - blue
// 0x02 color 2 - green
// 0x03 color 3 - cyan
// 0x04 color 4 - red
// 0x05 color 5 - magenta
// 0x06 color 6 - brown
// 0x07 color 7 - grey
// 0x08 color 8 - dark grey
// 0x09 color 9 - light blue
// 0x0A color 10 - light green
// 0x0B color 11 - light cyan
// 0x0C color 12 - light red
// 0x0D color 13 - pink
// 0x0E color 14 - yellow
// 0x0F color 15 - white

return (c % 2 ? 0x01 : 0x00) // blue bit (odd colors have blue)
+ (c==0x02 || c==0x03 || c==0x06 || c==0x07 || c==0x0A || c==0x0B || c==0x0E || c==0x0F ? 0x02 : 0x00) // green bit (colors 2,3,6,7,10,11,14,15 have green)
+ ((c>=0x04 && c<=0x07) || c>=0x0C ? 0x04 : 0x00) // red bit (colors 4,5,6,7,12,13,14,15 have red)
+ (c>0x07 ? 0x08 : 0x00); // intensity bit (colors greater than 7 have intensity)
}
void egaSetPixel(unsigned int x, unsigned int y, unsigned char c) {
// sets a pixel to the specified color at x,y
unsigned int memoffset = (x>>3)+(40*y); // from Lameguy64's EGA tutorial. This gets us the correct byte to write to by dividing x by 8 (x>>3) and adding (320/8)*y
// We still have to set the right bit within that byte.
// Note that the EGA card reads system RAM such that the leftmost bit is the leftmost pixel, for ease of understanding.
unsigned char whichBit = 0x80>>(x&0x7); // AND x with 00000111 (to get first 3 bits, i.e. remainder from the x>>3 operation) and shift 10000000 to the right that many times
unsigned char planes = egaGetPlanesForColor(c);
egaSelectPlane(~planes); // set the complement of the planes, i.e. all the ones we want to set 0 for
vmem[memoffset] = vmem[memoffset]&(~whichBit); // AND it with the complement of the bit, to set 0 for the planes we don't want
egaSelectPlane(planes); // set the planes we want to set 1 for
vmem[memoffset] = vmem[memoffset]|whichBit; // OR it with the bit to set 1 for the planes we do want
}

World's foremost 486 enjoyer.

Reply 9 of 24, by ViTi95

User metadata
Rank Oldbie
Rank
Oldbie

I'm trying to understand a bit the code, but seems a bit overcomplex just for writing a single pixel to EGA. With write mode 2 you just can set a single pixel in VRAM (or multiple):

  • Calculate the address in VRAM
  • Read the address so it's value gets copied in the latches (no need to store it in RAM)
  • Calculate the bitmask and set it (in order to select where data is copied from, part will be from the latched data and the other part will be from the CPU bus)
  • Write the color to the address

If you want to know more about EGA programming read this book, it's really good and helped me a lot to create FastDoom's EGA mode (http://vtda.org/books/Computing/Programming/E … DyckKliewer.pdf)

https://www.youtube.com/@viti95

Reply 10 of 24, by keenmaster486

User metadata
Rank l33t
Rank
l33t

I realized that with the default EGA palette the color value is actually the same value that my egaGetPlanesForColor function returns, i.e. it IS the value that you should write to the plane select register. The IBM engineers were way ahead of me.

That only sped it up by 10-20% though. Not sure what the slowest part of that function is.

I've heard write mode 2 is slower than write mode 0, though, is that true?

And then if I'm not mistaken, write mode 1 only allows you to copy memory within the card.

If I am wrong, sorry. I am learning all of this stuff for the first time and reading a lot of different resources all at once.

Edit: I think I am going about this in completely the wrong way. My function sets the entire byte every time, so the card will pick up all of those bits because there is no bitmask. Maybe write mode 0 is not meant for setting individual pixels at all.

World's foremost 486 enjoyer.

Reply 11 of 24, by ViTi95

User metadata
Rank Oldbie
Rank
Oldbie

In my testings write mode 0 and write mode 2 have very similar speeds, the main issue with EGA cards is the slow 8-bit ISA bus (~500Kb/s). The less you read/write to the card, the better.

Write mode 1 is designed to do VRAM-to-VRAM copies, but you also can do the same with Write mode 2. The only way to make an EGA card go fast is to be creative and use all the tricks in the book.

And yeah, setting a single pixel is always slow, as you are required to modify 8 pixels anyway.

https://www.youtube.com/@viti95

Reply 12 of 24, by cinnabar

User metadata
Rank Newbie
Rank
Newbie

First things first, selecting colours with the map mask register is not ideal. If you set all the pixels in a byte white (map mask = 0Fh) then when you subsequently try and set a pixel some other colour like blue, you are not making the data in the other planes go away by masking it - all a masked off plane does is disable writes to the plane, it doesn't reset any data. I guess that's why you're doing all that voodoo with complements of values and stuff?

For setting colours in write Mode #0 you can use the Enable Set/Reset and Set/Reset registers and you use the bitmask register for selecting pixels.

You're better off using Write Mode #2, it just saves you the bother of having to change the Set/Reset registers yourself but you have to load the latches in both cases to preserve pixels already on screen that are in the same byte. Writing a single pixel to a byte non destructively is a Read/Modify/Write operation. VGA has a nifty write mode #3 which offers a little more convenience still.

Be aware that if you're trying to read individual pixels you'll need to understand the two Read Modes and typically it is very slow. Generally you'll only read the EGA memory to load the latches.

Also you don't want the overhead of calling a function every time you modify a pixel, you'll usually be doing a lot of pixel reads/writes in a single loop rather than calling a function again and again for every minute thing.

You may want to think about learning assembler. It forces you to think at a hardware level. But another approach to optimisation is to tailor the product to the hardware. "Particle Effects" may sound nice, but is it really suited to the EGA? Maybe you should aim at a different card, or maybe tailor your game or production to the EGAs strengths instead of its weaknesess. After all there is a reason Duke Nukem has chunky byte aligned scrolling and uses a window rather than full screen and a reason Commander Keen hasn't many sprites on the go cos the EGA is terrible at doing non aligned graphics and terrible at software sprites.

Mastering the EGA isn't easy by any means, planar modes can drive you nuts, but I wish you the best of luck in your quest! The book ViTi95 mentions is good!

Reply 13 of 24, by keenmaster486

User metadata
Rank l33t
Rank
l33t

I know this is an old thread, but I figured out the plot pixel function.

The one I had posted above doesn't work right because it doesn't mask off the right bits. If you use any other color than white, it falls apart.

You have to read a byte to get the EGA card to load the planes for that byte into the latches, which it then uses for the "bitmask".

So here's the function in its working form:

void setPixel(unsigned int x, unsigned int y, unsigned char c) {
// sets a pixel to the specified color at x,y
unsigned int memoffset = (x>>3)+(40*y); // from Lameguy64's EGA tutorial. This gets us the correct byte to write to by dividing x by 8 (x>>3) and adding (320/8)*y
// This works because the memory is laid out bitwise rather than bytewise - the bits across the bytes represent pixels.
// So there are 40 columns of one byte width (8*40 = 320) across the screen itself.
// We still have to set the right bit within that byte.
// Note that the EGA card reads system RAM such that the leftmost bit is the leftmost pixel, for ease of understanding,
// so if you want to refer to the first pixel in a byte, you would use 0x80, i.e. 128, or 10000000.

unsigned char whichBit = (unsigned char)0x80>>(x&(unsigned int)7); // AND x with 00000000 00000111 (to get first 3 bits, i.e. remainder from the x>>3 operation) and shift 10000000 to the right that many times

// Read the byte from VRAM, causing the EGA card to load the plane bytes for it into the latches.
// The latches will be drawn from for the masked off areas (set by the bitmask) in order to maintain
// the existing values of the other pixels in the byte.
unsigned char oldByte = vmem[memoffset];

// set the bitmask:
egaSetBitmask(whichBit);

// Due to the design of the EGA color scheme, the composite planes value is *THE SAME* as the color value.
egaSetWritePlane(~c); // set the complement of the planes, i.e. all the ones we want to set 0 for.
vmem[memoffset] = oldByte&(~whichBit); // AND it with the complement of the bit, to set 0 for the planes we don't want
egaSetWritePlane(c); // set the planes we want to set 1 for
vmem[memoffset] = oldByte|whichBit; // OR it with the bit to set 1 for the planes we do want
}

Nearly as I can tell, this is the simplest way to plot a single pixel at a time, at least in write mode 0.

I think I understand how this works a little better now. I also wrote this function, which draws a 16x16 pixel solid color "tile" at a (j, k) position on a 16x grid:

void drawSolidColorTile(unsigned int j, unsigned int k, unsigned char c) {
unsigned int memoffset = (j<<1)+(640*k);
egaSetWritePlane(c);
for (unsigned int m = memoffset; m < memoffset + 640; m += 40) {
vmem[m] = 0xFF;
vmem[m + 1] = 0xFF;
}
}

This is in anticipation of writing a function to draw a single 16x16 tile from memory to the screen.

World's foremost 486 enjoyer.

Reply 14 of 24, by keenmaster486

User metadata
Rank l33t
Rank
l33t
// These page offsets give us a 16 pixel buffer around each page
unsigned int visiblePageOffset = 1104;
unsigned int drawPageOffset = 11312;
signed char horizPan = 0x00;

// ...

void egaSetLogicalLineLength(unsigned char l) {
outp(0x3D4, 0x13);
outp(0x3D5, l);
}

void egaSetHorizPan(unsigned char pixels) {
unsigned char temp = inp(0x3DA);
outp(0x3C0, 0x13);
outp(0x3C0, pixels);
}

void egaSetStartOffset(unsigned int offset) {
outp(0x3D4, 0x0C);
outp(0x3D5, (unsigned char)((offset >> 8) & 0xFF)); // high byte
outp(0x3D4, 0x0D);
outp(0x3D5, (unsigned char)(offset & 0xFF)); // low byte
}

void egaFlipPages() {
unsigned int temp = drawPageOffset;
drawPageOffset = visiblePageOffset;
visiblePageOffset = temp;
egaSetStartOffset(visiblePageOffset);
}

void egaShiftPages(int shift) {
if (drawPageOffset + shift < 0) {
drawPageOffset = 65535 + (drawPageOffset + shift);
} else if (drawPageOffset + shift > 65535) {
drawPageOffset = 65535 - (drawPageOffset + shift);
} else {
drawPageOffset += shift;
}

if (visiblePageOffset + shift < 0) {
visiblePageOffset = 65535 + (visiblePageOffset + shift);
} else if (visiblePageOffset + shift > 65535) {
visiblePageOffset = 65535 - (visiblePageOffset + shift);
} else {
visiblePageOffset += shift;
}
}

void egaScrollX(signed char x) {
horizPan += x;
if (horizPan < 0) {
horizPan += 8;
egaShiftPages(-1);
} else if (horizPan > 7) {
horizPan -= 8;
egaShiftPages(1);
}
egaSetHorizPan((unsigned char)horizPan);
Show last 17 lines
}

void egaScrollY(signed char y) {
int offsetAmount = 44*y;
egaShiftPages(offsetAmount);
}






void egaInit() {
egaSetStartOffset(visiblePageOffset);
egaSetLogicalLineLength(22); // 22 double words = 44 words = 44 bytes/plane = 44 columns = 352 pixels wide = 16 + 320 + 16 = one tilewidth on either side of the screen
}

Working on the scrolling code. This is all working as expected so far. Someone correct me if any of this is wrong.

I also had to modify the quasi-tile drawing code to handle wraparound and the buffer zone. I think this is very slow and unoptimized, but it works and I can understand it, and I can optimize later.

void drawSolidColorTile(int j, int k, unsigned char c) {
long memoffset = drawPageOffset + (j<<1)+(704*k);
if (memoffset < 0) {memoffset += 65535;}
if (memoffset > 65535) {memoffset -= 65535;}
egaSetWritePlane(c);
for (long m = memoffset; m < memoffset + 704; m += 44) {
long temp = m;
if (temp < 0) {temp += 65535;}
if (temp > 65535) {temp -= 65535;}
vmem[temp] = 0xFF;
long temp2 = m + 1;
if (temp2 < 0) {temp2 += 65535;}
if (temp2 > 65535) {temp2 -= 65535;}
vmem[temp2] = 0xFF;
}
}

World's foremost 486 enjoyer.

Reply 15 of 24, by pan069

User metadata
Rank Oldbie
Rank
Oldbie

Apologies upfront, I don't have an awful lot of time atm, life's getting in the way, so I am just scanning your code. At face value, looks good. Just one thing I noticed in your egaSetStartOffset function. Since you can't set the high and low byte with a single 16 bit write and you have to spread it out over two seperate writes, you typically want to disable interrupts before doing the writes and enable them again after writing the high and low byte values. If you don't do this you can end up with weird flickering as the function can be "interrupted" in between setting the high and low byte being set.

So, depending on which compiler you're using you could do something like:

void egaSetStartOffset(unsigned int offset) {
asm cli
outp(0x3D4, 0x0C);
outp(0x3D5, (unsigned char)((offset >> 8) & 0xFF)); // high byte
outp(0x3D4, 0x0D);
outp(0x3D5, (unsigned char)(offset & 0xFF)); // low byte
asm sti
}

From Abrash's Black Book:

For maximum safety, you should disable interrupts around the key portions of your page-flipping code, although here we run into the problem that if interrupts are disabled from the time we start looking for Display Enable until we set the Pel Panning register, they will be off for far too long, and keyboard, mouse, and network events will potentially be lost.

Source: https://www.phatcode.net/res/224/files/html/ch23/23-06.html

Reply 16 of 24, by keenmaster486

User metadata
Rank l33t
Rank
l33t
pan069 wrote on 2025-03-18, 09:29:

Apologies upfront, I don't have an awful lot of time atm, life's getting in the way, so I am just scanning your code. At face value, looks good. Just one thing I noticed in your egaSetStartOffset function. Since you can't set the high and low byte with a single 16 bit write and you have to spread it out over two seperate writes, you typically want to disable interrupts before doing the writes and enable them again after writing the high and low byte values. If you don't do this you can end up with weird flickering as the function can be "interrupted" in between setting the high and low byte being set.

Thank you, I went ahead and added that.

Right now I'm struggling to draw masked tiles. Unmasked is no issue, my unmasked code is working fine. Here's what I'm trying to do in pseudocode:

These are 16x16 pixel tiles, stored in arrays, BGRI(M) plane order, row major order within the planes.

Unmasked writes (working):

for (each plane) {
egaSetWritePlane(plane)
for (row) {
writeBothColumns
}
}

Masked writes (NOT working):

for (row) {
for (column) {
readByteFromVmem // load latches - right?
egaSetBitmask(mask byte from tile data)
for (each plane) {
egaSetWritePlane(plane)
writeData
}
}
}

I'm testing this code by drawing a screen full of unmasked tiles and then trying to draw a screen full of masked tiles on top of them.

I've tried mimicking my setPixel routine from above that seems to work fine... no matter what I do I get bizarre behavior that doesn't make any sense. It just corrupts the whole screen with something that looks vaguely similar to the desired outcome but as though the computer has had a stroke while trying to draw my tiles.

I have spent two days now searching the internet trying to find an example of this without any luck. No one seems to have ever encountered this before (even though I know hundreds of people have). Everyone who talks about it seems to take it for granted that the above method in my pseudocode will just work - but it does not.

What am I doing wrong?

World's foremost 486 enjoyer.

Reply 17 of 24, by keenmaster486

User metadata
Rank l33t
Rank
l33t

Alright I figured it out. You have to read the byte from the current memory location, AND it with the complement of the mask, OR it with the byte you're writing, and then write it back - essentially doing the masking yourself... *and* you have to use the bitmask register.

I really don't understand this at all.

World's foremost 486 enjoyer.

Reply 18 of 24, by mkarcher

User metadata
Rank l33t
Rank
l33t
keenmaster486 wrote on 2025-04-29, 20:01:
Masked writes (NOT working): […]
Show full quote

Masked writes (NOT working):

for (row) {
for (column) {
readByteFromVmem // load latches - right?
egaSetBitmask(mask byte from tile data)
for (each plane) {
egaSetWritePlane(plane)
writeData
}
}
}

This is supposed to work that way. Make sure you are in write mode 0, with no rotation enabled and combination function "0" (overwrite) active. Furthermore you need to have the enable set/reset register cleared. Most likely all of this is already the case though, because otherwise you would have issues with unmasked drawing as well. Maybe your "egaSetBitmask" function is misbehaving?

keenmaster486 wrote on 2025-04-29, 21:44:

Alright I figured it out. You have to read the byte from the current memory location, AND it with the complement of the mask, OR it with the byte you're writing, and then write it back - essentially doing the masking yourself... *and* you have to use the bitmask register.

If you do the masking yourself, you can have the bit mask register at a constant 0xFF. This should work as well, but it is slower than the algorithm you originally tried, as you need to select the read plane and read 3 extra times.

Reply 19 of 24, by keenmaster486

User metadata
Rank l33t
Rank
l33t
mkarcher wrote on 2025-04-29, 22:30:

This is supposed to work that way. Make sure you are in write mode 0, with no rotation enabled and combination function "0" (overwrite) active. Furthermore you need to have the enable set/reset register cleared. Most likely all of this is already the case though, because otherwise you would have issues with unmasked drawing as well. Maybe your "egaSetBitmask" function is misbehaving?

Hmm. Well, all of that is the case, and my egaSetBitmask is just a simple port write, nothing to it.

mkarcher wrote on 2025-04-29, 22:30:

If you do the masking yourself, you can have the bit mask register at a constant 0xFF. This should work as well, but it is slower than the algorithm you originally tried, as you need to select the read plane and read 3 extra times.

Well, I'm not selecting any read planes.

Here's what I currently have, and maybe you can tell me why this is the only way that seems to work (it does work now):

unsigned char p = 0x01;
unsigned char dataPos = 0;
unsigned char maskDataPos = 0;
for (unsigned int m = memoffset; m < memoffset + 704; m += 44) {
maskDataPos = dataPos + 128;
for (unsigned char i = 0; i < 2; i++) {
p = 0x01;
egaSetBitmask(this->data[t][maskDataPos + i]);
for (unsigned char offs = 0; offs < 128; offs += 32) {
egaSetWritePlane(p);
vmem[m + i] = (vmem[m + i] & ~this->data[t][maskDataPos + i])|this->data[t][dataPos + offs + i];
p <<= 1;
}
}
dataPos += 2;
}
egaSetBitmask(0xFF);

World's foremost 486 enjoyer.