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.
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:
1unsigned 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
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?
HanSolowrote 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:
1unsigned 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.
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.
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
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.
1void egaSelectPlane(unsigned char p) { 2 // p can have one of four values to select a single plane: 3 // 0x01 - plane 0 4 // 0x02 - plane 1 5 // 0x04 - plane 2 6 // 0x08 - plane 3 7 8 // Or any aggregation of those to select multiple planes 9 // Example: 10 // 0x0F - all planes 11 12 outp(0x3C4, 0x02); // select plane-select register 13 outp(0x3C5, p); 14} 15 16unsigned char egaGetPlanesForColor(unsigned char c) { 17 // gets a composite value for the planes you should write to for a particular color 18 // bitplanes: BGRI (0,1,2,3) 19 // plane 0 - blue 20 // plane 1 - green 21 // plane 2 - red 22 // plane 3 - intensity 23 24 // 0x00 color 0 - black 25 // 0x01 color 1 - blue 26 // 0x02 color 2 - green 27 // 0x03 color 3 - cyan 28 // 0x04 color 4 - red 29 // 0x05 color 5 - magenta 30 // 0x06 color 6 - brown 31 // 0x07 color 7 - grey 32 // 0x08 color 8 - dark grey 33 // 0x09 color 9 - light blue 34 // 0x0A color 10 - light green 35 // 0x0B color 11 - light cyan 36 // 0x0C color 12 - light red 37 // 0x0D color 13 - pink 38 // 0x0E color 14 - yellow 39 // 0x0F color 15 - white 40 41 return (c % 2 ? 0x01 : 0x00) // blue bit (odd colors have blue) 42 + (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) 43 + ((c>=0x04 && c<=0x07) || c>=0x0C ? 0x04 : 0x00) // red bit (colors 4,5,6,7,12,13,14,15 have red) 44 + (c>0x07 ? 0x08 : 0x00); // intensity bit (colors greater than 7 have intensity) 45} 46void egaSetPixel(unsigned int x, unsigned int y, unsigned char c) { 47 // sets a pixel to the specified color at x,y 48 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 49 // We still have to set the right bit within that byte. 50 // Note that the EGA card reads system RAM such that the leftmost bit is the leftmost pixel, for ease of understanding. 51 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 52 unsigned char planes = egaGetPlanesForColor(c); 53 egaSelectPlane(~planes); // set the complement of the planes, i.e. all the ones we want to set 0 for 54 vmem[memoffset] = vmem[memoffset]&(~whichBit); // AND it with the complement of the bit, to set 0 for the planes we don't want 55 egaSelectPlane(planes); // set the planes we want to set 1 for 56 vmem[memoffset] = vmem[memoffset]|whichBit; // OR it with the bit to set 1 for the planes we do want 57}
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)
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.
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.
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!
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:
1void setPixel(unsigned int x, unsigned int y, unsigned char c) { 2 // sets a pixel to the specified color at x,y 3 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 4 // This works because the memory is laid out bitwise rather than bytewise - the bits across the bytes represent pixels. 5 // So there are 40 columns of one byte width (8*40 = 320) across the screen itself. 6 // We still have to set the right bit within that byte. 7 // Note that the EGA card reads system RAM such that the leftmost bit is the leftmost pixel, for ease of understanding, 8 // so if you want to refer to the first pixel in a byte, you would use 0x80, i.e. 128, or 10000000. 9 10 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 11 12 // Read the byte from VRAM, causing the EGA card to load the plane bytes for it into the latches. 13 // The latches will be drawn from for the masked off areas (set by the bitmask) in order to maintain 14 // the existing values of the other pixels in the byte. 15 unsigned char oldByte = vmem[memoffset]; 16 17 // set the bitmask: 18 egaSetBitmask(whichBit); 19 20 // Due to the design of the EGA color scheme, the composite planes value is *THE SAME* as the color value. 21 egaSetWritePlane(~c); // set the complement of the planes, i.e. all the ones we want to set 0 for. 22 vmem[memoffset] = oldByte&(~whichBit); // AND it with the complement of the bit, to set 0 for the planes we don't want 23 egaSetWritePlane(c); // set the planes we want to set 1 for 24 vmem[memoffset] = oldByte|whichBit; // OR it with the bit to set 1 for the planes we do want 25}
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:
1void drawSolidColorTile(unsigned int j, unsigned int k, unsigned char c) { 2 unsigned int memoffset = (j<<1)+(640*k); 3 egaSetWritePlane(c); 4 for (unsigned int m = memoffset; m < memoffset + 640; m += 40) { 5 vmem[m] = 0xFF; 6 vmem[m + 1] = 0xFF; 7 } 8}
This is in anticipation of writing a function to draw a single 16x16 tile from memory to the screen.
61} 62 63void egaScrollY(signed char y) { 64 int offsetAmount = 44*y; 65 egaShiftPages(offsetAmount); 66} 67 68 69 70 71 72 73void egaInit() { 74 egaSetStartOffset(visiblePageOffset); 75 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 76}
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.
1void drawSolidColorTile(int j, int k, unsigned char c) { 2 long memoffset = drawPageOffset + (j<<1)+(704*k); 3 if (memoffset < 0) {memoffset += 65535;} 4 if (memoffset > 65535) {memoffset -= 65535;} 5 egaSetWritePlane(c); 6 for (long m = memoffset; m < memoffset + 704; m += 44) { 7 long temp = m; 8 if (temp < 0) {temp += 65535;} 9 if (temp > 65535) {temp -= 65535;} 10 vmem[temp] = 0xFF; 11 long temp2 = m + 1; 12 if (temp2 < 0) {temp2 += 65535;} 13 if (temp2 > 65535) {temp2 -= 65535;} 14 vmem[temp2] = 0xFF; 15 } 16}
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:
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.
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.
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.
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.
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?
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.
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.
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):
1unsigned char p = 0x01; 2unsigned char dataPos = 0; 3unsigned char maskDataPos = 0; 4for (unsigned int m = memoffset; m < memoffset + 704; m += 44) { 5 maskDataPos = dataPos + 128; 6 for (unsigned char i = 0; i < 2; i++) { 7 p = 0x01; 8 egaSetBitmask(this->data[t][maskDataPos + i]); 9 for (unsigned char offs = 0; offs < 128; offs += 32) { 10 egaSetWritePlane(p); 11 vmem[m + i] = (vmem[m + i] & ~this->data[t][maskDataPos + i])|this->data[t][dataPos + offs + i]; 12 p <<= 1; 13 } 14 } 15 dataPos += 2; 16} 17egaSetBitmask(0xFF);