Image Map Image Map
Results 1 to 7 of 7

Thread: writing byte to EGA VRAM produces unintended result

  1. #1

    Question writing byte to EGA VRAM produces unintended result

    Hi everyone, I'm doing some hobby MS-DOS game programming and my current project is going to support the EGA. I'm learning how to program it from the IBM technical manual, which to be honest has been quite difficult.
    Just to see if I understand the hardware, I've decided to write a simple program that draws 2 8x8 rectangles with the overall goal of: correct pixel-alignment, and second rectangle over-writes part of the first.
    Here's my code and a screenshot of what it looks like.
    Code:
    .model small
    .stack 100h
    .386
    
    .data
            current_page    WORD    0
            x_pos           WORD    0
            x_pos_2         WORD    1
            y_pos           WORD    120
            y_pos_2         WORD    124
    .code
    SC_INDEX        	EQU     03C4h
    CRTC_INDEX      	EQU     03D4h
    GC_ADDR_REG             EQU     03CEh
    EGA_MODE_REG		EQU	05h
    EGA_DATA_ROTATE 	EQU     03h
    EGA_MAP_MASK    	EQU     02h
    EGA_WRITE_MASK          EQU     08h
    EGA_XOR_LATCH           EQU     00011000b
    EGA_WRITE_MODE_1	EQU	00000001b
    EGA_READ_MODE_0		EQU	00000000b
    EGA_PAGE_SIZE   	EQU     8000
    SCREEN_WIDTH    	EQU     40
    SCREEN_HEIGHT   	EQU     200
    
    ;EGA colors masks for the default palette
    EGA_CYAN        EQU     00000011b
    EGA_BLUE        EQU     00000001b
    EGA_GREY        EQU     00001000b
    EGA_BLACK       EQU     00000000b
    EGA_BROWN       EQU     00000110b
    EGA_MAGENTA     EQU     00000101b
    EGA_RED         EQU     00000100b
    EGA_BRITE_GREY  EQU     00000111b
    EGA_BRITE_CYAN  EQU     00001011b
    EGA_BRITE_GREEN EQU     00001010b
    EGA_BRITE_BLUE  EQU     00001001b
    EGA_BRITE_BLUE  EQU     00001001b
    EGA_BRITE_RED   EQU     00001100b
    EGA_PINK        EQU     00001101b
    EGA_YELLOW      EQU     00001110b
    EGA_WHITE       EQU     00001111b
    
    
    main PROC
            mov ax, @data
            mov ds, ax
    
            xor ax, ax
            mov al, 0Dh
            int 10h
            mov ax, 0A000h
            mov es, ax
    
            mov al, EGA_WHITE
            call EGA_set_color_mask
    
            mov bx, 0
            mov cx, EGA_PAGE_SIZE 
            mov al, 00h
    FillScreen:
            mov es:[bx], al
            inc bx
            loop FillScreen
    
            mov ax, x_pos
            mov bx, y_pos
            mov cx, EGA_BLUE
            call EGA_draw_rect_8x8
    
            mov ax, x_pos_2
            mov bx, y_pos_2
            mov cx, EGA_RED
            call EGA_draw_rect_8x8
    GameLoop:
    ;check for a key press
            mov ah, 01h
    	int 16h
            jnz ExitGame
    
            jmp GameLoop
    
    ExitGame:
            xor ax, ax
            mov al, 03h
            int 10h
            mov ax, 4C00h
            int 21h
    main ENDP
    
    
    ;****************************************************
    EGA_set_color_mask PROC
    ;sets the color mask for the vga card
    ;       al = color mask to use (see EGA color definitions for values)
    ;****************************************************
    	push dx
            push ax
            mov dx, SC_INDEX
            mov al, EGA_MAP_MASK
            out dx, al
            inc dx
            pop ax
            out dx, al
    
            pop dx
            ret
    ega_set_color_mask ENDP
    
    ;****************************************************
    EGA_draw_rect_8x8 PROC
    ;draws a (hopefully) pixel-aligned 8x8 rectangle on screen
    ;procedure assues es holds VRAM segment
    ;       AX = x coordinate in screen coordinates
    ;       BX = y coordinate in screen coordinates
    ;       CL = ega color mask to use
    ;****************************************************
            push dx
    	push ax
    
    ;select write mode 1, XOR data operation
    	mov dx, GC_ADDR_REG
    	mov al, EGA_DATA_ROTATE
    	out dx, al
    	inc dx
    	mov al, EGA_XOR_LATCH
    	out dx, al
    
    	mov	al, EGA_WRITE_MASK	;set write mask
    	out	dx, al
    	inc	dx
    	mov	al, 0FFh
    	out	dx, al
    	
    	dec dx
    	mov al, EGA_MODE_REG
    	out dx, al
    	inc dx
            mov al, EGA_WRITE_MODE_1
    	out dx, al
    
    	pop ax
    
            mov dx, ax              ;save x coordinate
            mov al, cl
            call EGA_set_color_mask
    
            mov ax, bx              ;set ax to y coordinate
            mov bl, SCREEN_WIDTH
            mul bl
            push ax
            mov ax, dx              ;divide the x coordinate by 8
            shr ax, 1
            shr ax, 1
            shr ax, 1
            mov bx, ax              ;save new x in bx
            pop ax
            add ax, bx              ;final VRAM offset...
            mov di, ax              ;... in destination index
    
            mov cx, 8               ;8 rows
            mov bx, 0
    DrawLoop:
            mov ax, 0F000h      ;just a test value to see what actually appears on screen
            mov es:[di+bx], ah
            mov es:[di+bx+1], al
            add bx, 40
            loop DrawLoop
    
            pop dx
            ret
    EGA_draw_rect_8x8 ENDP
    
    END main
    2019-09-04 19_05_29-DOSBox Debugger.jpg

    It might be a bit hard to see in the screenshot but basically, Im setting the value to display on screen as 11110000 binary but what appears on screen looks more like 1000 1110. I think.
    The function that does the actual drawing is on the bottom but I included everything just incase I was doing something wrong beforehand.

  2. #2

    Default

    Let's take a look at what you are actually doing here:

    Quote Originally Posted by sudo459 View Post
    Code:
    	mov dx, GC_ADDR_REG
    	mov al, EGA_DATA_ROTATE
    	out dx, al
    	inc dx
    	mov al, EGA_XOR_LATCH
    	out dx, al
    Output 0x03 to port 0x3ce and then 0x18 to port 0x3cf. This will set the Data Rotate field to 0 (as usual) and the Logic Unit Function Code field to 3, meaning the value you write will be XORed with the value in the read latch (depending on write mode). This is a pretty advanced thing to do, I suspect it is not what you want, and that you really want to leave the Logic Unit Function Code field set to 0 (No-op) as it is normally.

    Quote Originally Posted by sudo459 View Post
    Code:
    	mov	al, EGA_WRITE_MASK	;set write mask
    	out	dx, al
    	inc	dx
    	mov	al, 0FFh
    	out	dx, al
    Note that DX is still 0x3cf at the start of this section, so this will write 0x08 to port 0x3cf and 0xff to port 0x3d0 (which is not a standard VGA register). As the graphics controller register is still set to Data Rotate, this will set the Logic Unit Function Code field to 1 (AND), which again isn't really what you want I think. If you do a "dec dx" at the start, you'll set the Bit Mask Register to 0xff which seems like a more sensible thing to do.

    Quote Originally Posted by sudo459 View Post
    Code:
    	dec dx
    	mov al, EGA_MODE_REG
    	out dx, al
    	inc dx
            mov al, EGA_WRITE_MODE_1
    	out dx, al
    Again this will write to ports 0x3cf and 0x3d0, probably not what you want. It will set the Data Rotate field to 5 and the Logic Unit Function to 0 (no-op, accidentally the right thing). This data rotate value explains the odd behaviour that you are seeing, I think. If dx was 0x3ce at the start of this section (i.e. you had decremented it at the beginning of the second section) you would be setting the Processor Write Mode field to 1. I'm not sure why you think you want to do this - mode 1 is for copying all four planes at once from one VRAM location to another (the CPU issues a read, the VRAM bits from that address go into an internal latch, the CPU issues a write and the internal latch values then are written back into VRAM at the write location - the data that the CPU writes isn't used). You probably want to use write mode 0, if you just want to draw stuff on the screen.

    Quote Originally Posted by sudo459 View Post
    [CODE]
    mov es:[di+bx], ah
    mov es:[di+bx+1], al
    ...and you are never actually issuing a read so you'll be writing whatever data is left in the latch from the last read operation that occurred before you code even ran.

    Hopefully by fixing those problems (no-op ALU op, decrement dx, write mode 0) you'll be able to draw the thing you are trying to draw. Looking forward to seeing how the game turns out!

  3. #3

    Default

    Hey, thanks for the reply! I actually saw that I wasn't writing to the correct port, and also setting the rotation on the graphics controller instead of the mode. Also, it does seem that I do not want to use mode 1 for writing. I worked on it some more and got it to do what I want it to, but, theres a lot of outs, and bit fiddling, making me a bit weary about performance (Im targeting a 80286). One thing I was thinking of is, since I'm using 320x200 mode, I can use another page for double-buffering, and use the rest of the video memory to store sprite data? I could use write mode 1 to load it, then draw it correct? But then how would I go about making sure it can be pixel-aligned?
    A better question is, whats the standard way to draw sprites to the EGA that are pixel-aligned and are fast?

    Heres my updated code:
    Code:
    ;****************************************************
    EGA_draw_rect_8x8 PROC
    ;draws a pixel-aligned 8x8 rectangle (square) on screen
    ;procedure assues es holds VRAM segment
    ;       AX = x coordinate in screen coordinates
    ;       BX = y coordinate in screen coordinates
    ;       CL = ega color mask to use
    ;****************************************************
    
    ;First we need to find the offset in VRAM to draw
    ;offset = y * w + (x / 8)
            push dx         ;this is modified by the mul we use,
                            ;remove it when the mul is changed to shifts
            push ax
            mov ax, bx      ;ax needs to hold the y so we can use mul
            mov bx, SCREEN_WIDTH
            mul bx          ;need to change this to shifts instead
            pop bx          ;bx holds the x
    	push bx		;save x again, bx will hold x/8
            mov ch, cl      ;save the color since we will use cl
            mov cl, 3
            shr bx, cl      ;divide x by 8
            add ax, bx
            mov di, ax      ;di now holds the offset 
    
    ;Then we need to calculate the bitmask to out to the EGA
            and cx, 0FF00h  ;we need to save ch, but push uses a 16bit reg
    	pop ax
            push cx
            mov cx, ax
            and cx, 7       ;performs CX mod 8
    	mov ax, 0FF00h
            shr ax, cl      ;ax holds the bitmask now
            pop cx          ;ch now holds the color again
            
    ;stack now holds: dx, return addr
    ;registers hold: ax = bitmask, bx = x, ch = color mask, di = VRAM offset
    ;now we need to set the bitmask.
    ;This must be done twice, once for the first byte and once for the second
            push ax         ;save the bitmask, we need al
            mov dx, GC_ADDR_REG
            mov al, EGA_BIT_MASK
            out dx, al
            inc dx
            pop ax
            xchg ah, al     ;al now holds mask for first byte, ah for second
            out dx, al
    ;Bit mask set. We are set to begin drawing. First, clear all planes to 0
            mov dx, SC_INDEX
            mov al, EGA_MAP_MASK
            out dx, al
            inc dx
            mov al, 0FFh
            out dx, al
    
            mov al, 00h
            push bx         ;we will use bx
            push cx
            mov bx, 0       ;bx will be used for a scanline counter
            mov cx, 8       ;erase 8 scanlines
    DrawLoop1:
            mov dh, es:[di+bx]      ;load the EGA latches, we don't care about
                                    ;the actual value
            mov es:[di+bx], al      ;only the bits unset by the mask will be
                                    ;written
            add bx, SCREEN_WIDTH
            loop DrawLoop1
    
    ;All planes set to zero. Now we redraw with only the planes selected we want
    ;first we need to select the planes we want to write to, ch holds this value
            pop cx
            mov dx, SC_INDEX
            mov al, EGA_MAP_MASK
            out dx, al
            inc dx
            mov al, ch
            out dx, al
    
            mov al, 0FFh
            mov bx, 0
            push cx
            mov cx, 8
    DrawLoop3:
            mov dh, es:[di+bx]      ;load the EGA latches, we don't care about
                                    ;the actual value
            mov es:[di+bx], al      ;only the bits unset by the mask will be
                                    ;written
            add bx, SCREEN_WIDTH
            loop DrawLoop3
    
    ;Now for the second byte. First the bitmask... AH holds it
            mov dx, GC_ADDR_REG
            mov al, EGA_BIT_MASK
            out dx, al
            inc dx
            xchg al, ah     ;al now holds mask for second byte
            out dx, al
    
    ;Erase second byte. First, reset the mask
            mov dx, SC_INDEX
            mov al, EGA_MAP_MASK
            out dx, al
            inc dx
            mov al, 0FFh            ;all planes
            out dx, al
    
            mov al, 00h
            mov bx, 0
            mov cx, 8
    DrawLoop2:
            mov dh, es:[di+bx+1]      ;load the EGA latches, we don't care about
                                    ;the actual value
            mov es:[di+bx+1], al
            add bx, SCREEN_WIDTH
            loop DrawLoop2
    
    ;Now draw second byte. Reset map mask
            pop cx
            mov dx, SC_INDEX
            mov al, EGA_MAP_MASK
            out dx, al
            inc dx
            mov al, ch
            out dx, al
    
            mov al, 0FFh
            mov bx, 0
            mov cx, 8
    DrawLoop4:
            mov dh, es:[di+bx+1]      ;load the EGA latches, we don't care about
                                    ;the actual value
            mov es:[di+bx+1], al
            add bx, SCREEN_WIDTH
            loop DrawLoop4
    
            pop bx
            pop dx
            ret
    EGA_draw_rect_8x8 ENDP

  4. #4

    Default

    Quote Originally Posted by sudo459 View Post
    I worked on it some more and got it to do what I want it to, but, theres a lot of outs, and bit fiddling, making me a bit weary about performance (Im targeting a 80286).
    Yes, to get high performance code here it's important to be aware of what the bottlenecks are and how to minimise the number of port writes. So you'll probably want to organise your code to do all plane 0 writes, then all the plane 1 writes and so on.

    Quote Originally Posted by sudo459 View Post
    One thing I was thinking of is, since I'm using 320x200 mode, I can use another page for double-buffering, and use the rest of the video memory to store sprite data?
    Are you targeting 64KB EGA cards or only 128KB EGA cards? With two buffers and 320x200x16, a 64KB EGA card only has room for another 1536 bytes of space for sprites unless you reduce the visible area.

    Quote Originally Posted by sudo459 View Post
    I could use write mode 1 to load it, then draw it correct?
    Write mode 0 to load your sprite into all planes of the sprite space part of VRAM in the first place, then mode 1 to copy it from there to your offscreen buffer quickly.

    Quote Originally Posted by sudo459 View Post
    But then how would I go about making sure it can be pixel-aligned?
    The fastest way would probably be to have 8 copies of your sprite, one for each pixel offset. The data rotate register seems like it might help but that can only be used in write mode 0, so doesn't help if you're copying from one VRAM location to another (write mode 1). Another possibility is write mode 2 where you write one byte (but only the lower 4 bits are used) for each pixel. I haven't played with this myself, but I think you'll need to output to a port (I'm not sure if it's the data rotate register or the bit mask register or both) to select which column-of-8 you're writing pixels to. Either way, to minimize the number of port writes, you should do all the 0 bits (leftmost within byte) for the entire sprite, then all the 1 bits and so on. Again you can't do the fast VRAM-to-VRAM copies if you're using write mode 2.

    Quote Originally Posted by sudo459 View Post
    A better question is, whats the standard way to draw sprites to the EGA that are pixel-aligned and are fast?
    I'd say the 8 copies method and write mode 1 would be the fastest and most standard. Write modes 2 was rarely used as far as I know, so might not even work the same way on all cards.

    Depending on your game, you might not need 8 copies - if you don't mind a coarser movement you might be able to get away with 4 or 2. And if you can synchronize your horizontal movement with your walk animation then the extra copies may not actually require any additional memory. I'm not 100% sure, but I have a feeling that Lemmings might work like this.

  5. #5

    Default

    Quote Originally Posted by reenigne View Post
    I'd say the 8 copies method and write mode 1 would be the fastest and most standard. Write modes 2 was rarely used as far as I know, so might not even work the same way on all cards.

    Depending on your game, you might not need 8 copies - if you don't mind a coarser movement you might be able to get away with 4 or 2. And if you can synchronize your horizontal movement with your walk animation then the extra copies may not actually require any additional memory. I'm not 100% sure, but I have a feeling that Lemmings might work like this.
    That sounds like the way I should program it. I'm targeting a 128k EGA (were those common back in the day? I read 128k were the most common).

    But to be honest, while I haven't "given up" per se, I've decided to roll back in history a bit and target the original IBM PC, running a CGA. The CGA has proven to be a lot easier to program for, and I'm also using the "multiple sprites per pixel offset" approach. As I understand, games of that era were "bootloaders". So I've decided to go that route, although the game will also be available in a DOS-compatible ".com" format. Writing the bootloader has proven a major challenge, and I'm running into an issue that really makes no sense to me. My bootloader contains a function that prints a hex number as a string. However, the function just seems to return in the middle of it, and doesn't fully run.

    Output on screen: "Loading from disk...Error reading disk." Here's the code: (written in NASM)
    Code:
    bits 16
    org 7C00h
    program_start:
    ;First 512 bytes are the boot sector
            mov ax, cs
            add ax, 512
            mov ss, ax
            mov sp, 4096
    
            mov ax, cs
            mov ds, ax
            mov es, ax
            mov bx, main_start
    
            push dx
            mov si, str_load
            call print_string
            
    
    .read_disk_loop:
            pop dx
            push dx
            mov dh, 1
            call read_from_disk
            cmp al, 1
            je .hang         
            add bx, 512
            jmp .read_disk_loop
    
            jmp main_start
    .hang:
            jmp .hang
    
    exit_program:
            jmp exit_program
    ;***********************
    print_string:
    ;prints the string pointed to by si
    ;string should be null-terminated
    ;***********************
    .printchar:
            mov al, [si]
            cmp al, 0
            je .done
            inc si
            mov bh, 0
            mov ah, 0Eh
            int 10h
            jmp .printchar
    .done:
            ret
    
    ;***********************
    read_from_disk:
    ;reads the data off of the rest of the disk
    ;attempts to read the disk 3 times
    ;       dh = number of sectors to read
    ;       es:bx = location to write data to
    ;       returns al, 0 = no error, 1 = error
    ;***********************
            push dx
            mov ah, 02h
            mov al, dh
            mov cl, 02h
            mov ch, 0
            int 13h
            jc .disk_error
    
            pop dx
            cmp al, dh
            jne .disk_error
    
            mov al, 0
            ret
    .disk_error:
            xor ah, ah
            mov dx, ax
            call print_hex
            mov si, str_error
            call print_string
            mov al, 1
            ret
    
    ;***************************
    print_hex:
    ;converts the value in dx into a printable string, then prints it
    ;       dx = input value
    ;***************************
            mov cx, 0
    .hex_loop:
            mov ax, dx
            and ax, 000Fh
            add al, 30h
            cmp al, 39h
    
                                              <------I added a print_string call here, and it successfully prints to the screen
    
            mov bx, hex_out+5
            sub bx, cx
            ror dx, 4
                                              <-----Adding a print_string call here results in nothing. It doesn't get executed
            jmp .hex_loop
    .end:
            mov si, hex_out            <-----This line is never executed
            call print_string
    
            ret
    
    hex_out: db "0x0000", 0
    str_load: db "Loading disk data...", 0
    str_success: db "Launching game.", 0

  6. #6

    Default

    Quote Originally Posted by sudo459 View Post
    That sounds like the way I should program it. I'm targeting a 128k EGA (were those common back in the day? I read 128k were the most common).
    Yes, 128kB was the most common but some early ones only had 64kB.

    Quote Originally Posted by sudo459 View Post
    As I understand, games of that era were "bootloaders".
    Some were, not all.

    Quote Originally Posted by sudo459 View Post
    Code:
            ror dx, 4
    I think this is the problem, assuming you're targeting 8088/8086. The form of this instruction that takes a immediate rotation count was not introduced until the 80186, so you need to use 4 copies of "ror dx,1" or rearrange your register usage and use "ror dx,cl".

  7. #7

    Default

    Quote Originally Posted by sudo459 View Post
    That sounds like the way I should program it. I'm targeting a 128k EGA (were those common back in the day? I read 128k were the most common).

    But to be honest, while I haven't "given up" per se, I've decided to roll back in history a bit and target the original IBM PC, running a CGA. The CGA has proven to be a lot easier to program for, and I'm also using the "multiple sprites per pixel offset" approach.
    Hi sudo459-

    reenigne certainly has a lot of experience with coding for both, but I'd add that although the CGA may have a simpler programming model, it doesn't come without challenges. Color is a big issue. The standard CGA palette options in 320x200 mode are pretty awful. To get more colors on-screen, you'd have to go with NTSC artifact colors (which only work on NTSC monitors, obviously) or use the special 160x100 16 color mode (modified 80 column text mode). You would also forgo hardware page flipping and would need to work around screen "snow" if you chose the 160x100 mode. Another possibility would be to lower the resolution even further and work with 80x100 or even 80x50 for a truly low-resolution screen. The benefits would be hardware page flipping and no "snow" at the expense of a very blocky image.

    On the other hand, EGA may have a little more initial programming effort to get working correctly, but you have a solid 320x200 16 color mode.

    At the expense of tooting my own horn, I've written a simple library that outputs to CGA, EGA and VGA using the 320x200 resolution on all the adapters. There are just three primitives (four if you include the alpha pixel, but that isn't relevant here) implemented using two fill modes: a patterned brush and solid color. The horizontal span primitive probably being the most interesting.

    On the EGA, the brush pattern is held in off-screen memory, just like a sprite would be. It could be a starting point for your own routines: https://github.com/dschmenk/Bresen-S...b/PIXSPAN4.ASM

    The CGA code is simpler, but has to fake some things like hardware page flipping and color: https://github.com/dschmenk/Bresen-S...b/PIXSPAN2.ASM

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •