VOGONS


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 12, 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 12, 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 3 of 12, by llm

User metadata
Rank Member
Rank
Member

Lameguy64 wrote a small (Watcom C) program to test out EGA stuff, accessing 0xA000:0 directly like you want

http://lameguy64.net/?page=dosstuff
http://lameguy64.net/snippets/EGATEST.C

compiled for me with: https://github.com/open-watcom/open-watcom-v2 without a problem

Reply 4 of 12, 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 12, 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 12, 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 8 of 12, 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 12, by ViTi95

User metadata
Rank Member
Rank
Member

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 12, 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 12, by ViTi95

User metadata
Rank Member
Rank
Member

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 12, 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!