VGA-Mode 13h


Part 2: of the asm-8086/88 Series


July 20, 2024 - Tommy Dräger


In our previous article, we set up a solid boilercode to get started with ASM-8086/88. This time, we're going to dive deeper into assembly and start to render a small 320x200 window. First let's clone our startup enviroment so that you can follow along:

Installation

git clone [email protected]:MilesTails01/i8086_boilercode.git

after downloading you need to unpack DOSBOX.zip and TOOLS.zip

cd \bin
tar -xf TOOLS.zip
tar -xf DOSBOX.zip

Usage

build.bat   // Compile, Link and Start the source
debug.bat   // Starts AFDEBUG and loads the compiled exe
start.bat   // Start the program without rebuilding it

Boilercode

;   #############################
;   #           MAIN.ASM        #
;   #############################

;   #############################
;   #           STACK           #
;   #############################
STACK SEGMENT PARA STACK
    db 64 dup(0)
STACK ENDS

;   #############################
;   #           DATA            #
;   #############################
DATA SEGMENT PARA 'DATA'
    string db 'Hello World','$'
    include \src\utils.inc
DATA ENDS

;   #############################
;   #           CODE            #
;   #############################
CODE SEGMENT PARA 'CODE'
    ASSUME cs:CODE, ds:DATA, ss:STACK

startup:
    mov ax, DATA
    mov ds, ax
    mov ax, STACK
    mov ss, ax

    lea dx, string
    call PRINT

    mov ah, 4Ch
    int 21h

CODE ENDS
END startup

VGA Mode

before the era of high-definition monitors and ultra-realistic graphics, computers displayed images on screens using VGA (Video Graphics Array). Introduced by IBM in 1987, VGA became the standard for PC graphics. It supported a resolution of 640x480 pixels with 16 colors or 320x200 pixels with 256 colors, among others. It provided a standardized way to handle graphics, making it easier for programmers to create visually appealing applications. VGA mode 13h, in particular, was a game-changer. It allowed for a 320x200 resolution with 256 colors, making it ideal for gaming and other graphic-intensive applications.

Early computer graphics were anything but straightforward. Programmers had to deal with limited memory and slow processing power. Plotting a single pixel on the screen required some research and good knowledge of hardware registers and a understanding of how the display memory worked.

To see a list of all available videomodes:

To change the Videomode with int 10h

so in order to start plotting pixel on our screen we will set the videomode to the famous mode 13h like this:

SET_VIDEO_MODE PROC 
    mov ax, VIDEOMODE_320x200   ;set video mode
    int 10h                     ;10h bios interupt
    ret
SET_VIDEO_MODE ENDP

We should also define some variables before progressing any further

DATA SEGMENT PARA 'DATA'
    MSG                 db 'Hello World','$'
    VIDEOMODE_320x200   dw 13h
    VIDEOMODE_640x480   dw 12h
    VID_MEM_SEG         dw 0A000h
    VID_MEM_OFF         dw 0000h
    VID_WIDTH           dw 320
    VID_HEIGHT          dw 200
DATA ENDS

As you can see we also defined Video Memory Segment (0xA000) and Offset (0x0000) as the documentation is telling us that the video memory address of the screen in 320x200 is starting at (A000:0000) (es:di).

In the future I will also explain how to use higher resoltion, but for now let's stick to 320x200. If I would choose a higher resolution like 640x480, I would have to swap banks in order to render correctly. As you can 320 * 200 = 64.000 (FA00h) which is barely enough to fit inside a 16-bit Registry like di. So the video memory is ranging from (A000:0000) to (A000:FA00) which is high performant and less complicated, hence the popularity among game developers back in the days.

Fill The Screen

So until now we have nothing but a blank screen in 320x200 resolution, nothing fancy. So let's move on and put some color on the screen. Just as a reminder: We have 64.000 Byte at the es:di address A000:0000 which are currently all at 00h. We cant control the color by passing RGB Values but we have to use an index rangin 0 - 255. The command in asm to store a single byte in memory would stosb. The color index have to be stored in al, then the value of al would be stored at the current es:di address in memory. If we are using rep stosb, he would do that store action cx - times, and he would also automatitcly count up the di register by 1 byte everytime.

REFRESH PROC NEAR       
    mov ax, VID_MEM_SEG ; ax = segment for display memory
    mov di, VID_MEM_OFF ; di = segment for display memory
    mov es, ax          ; es:di = address for display memory
    mov dx, VID_HEIGHT  ; vertical counter

FILL:
    mov ax, dx          ; pixel color is stored in al
    mov cx, VID_WIDTH   ; horizontal counter
    rep stosb           ; store al at es:di, cx times
    dec dx              ; decrement dx 1
    cmp dx, 0000h       ; is dx == 0 ?
    jne FILL            ; no ? then go on to the next line

    ret
REFRESH ENDP

As you can see the pixel color is defined by the current row we are currently at giving us the famous VGA rainbow screen. This Screenfill routine works fine for static screen. But as soon as we put the REFRESH procedure into a game loop the screen will start to flicker!

Double Buffering

We currently have a REFRESH procedure that is creating a flickering image. In order to avoid the flickering effect, we need to do 2 things: One we have to store the pixel data in another part of memory to keep the user from seeing the image buildup. So in other words we store the pixel data in a buffer sized 64.000 Byte and then copy it into memory when finished. And second we need to perfectly time the moment we copy the buffer into video memory by waiting for the vertical retrace signal to elimnate the flickering!

Usally Double Buffering is done by changing the address of the video memory every frame, but VGAs video memory address is unfortunatly not reprogrammable, so we have to copy the data. The is smart solution though which is called "mode-x" or "unchained-mode" which is described here.

So let's refactor our logic by creating a new procedure called BUFFER_TO_VRAM by using a buffer and waiting for VRETRACE Signal:

Have a look at VGA Documentation I found:

the documentation is claiming that port 0x3da holds a status byte where bit number 4 is stating if the Display is currently is Vertical Retrace!. So we have to read the byte from 0x3da, store it into ax, and make a bitwise to check if we are currently in VRETRACE.

DATA SEGMENT PARA 'DATA'
    BUFFER              db 64000 dup(0)
BUFFER_TO_VRAM PROC NEAR
    ; VSYNCH
    mov dx, 3dah            ; address of VGA Input Status #1 Register

    _EndVR:
    in al, dx               ; read from port 3dah into al
    test al, 08h            ; al & 00001000
    jnz _EndVR              ; is bit number 4 set (VRETRACE is ON)?

    _NewVR:
    in al, dx               ; read from port 3dah into al
    test al, 08h            ; al & 00001000
    jz _NewVR               ; is VRETRACE finsihed?

    xor si, si              ; si:si = address for buffer xor si, si = 0
    mov ax, VID_MEM_SEG     ; ax = segment for display memory
    mov di, VID_MEM_OFF     ; di = segment for display memory
    mov es, ax              ; es:di = address for display memory
    cld                     ; Make sure direction flag is clear
    mov cx,64000            ; cx = number of bytes to copy 320*200
    rep movsb               ; Copy cx 320*200 bytes from buffer to display memory   
ret
BUFFER_TO_VRAM ENDP

so this function will now copy whatever is in the Buffer into video memory, while also making sure the timing is correct. From now its just a matter of filling the Buffer with pixel data that we want to store into video memory.

FILL_BUFFER_XOR PROC NEAR
    mov di, 0                       ; Start at the beginning of the buffer
    mov ax, ds                      ; Copy the segment address of the data segment
    mov es, ax                      ; into the extra segment register
    mov dx, 0000h                   ; Set y coordinate to 0
FILL_Y:
    mov cx, 0000h                   ; Set x coordinate to 0
    FILL_X:
        mov ax, cx                  ; Move x coordinate into ax
        xor ax, dx                  ; XOR with y coordinate
        mov byte ptr es:[di], al    ; Store the result in the buffer
        inc di                      ; Move to the next byte in the buffer
        inc cx                      ; Increment x coordinate
        cmp cx, VID_WIDTH
        jne FILL_X

    inc dx                          ; Increment y coordinate
    cmp dx, VID_HEIGHT
    jne FILL_Y
    ret
FILL_BUFFER_XOR ENDP

For the Fill Function I thought of a XOR Pattern. In other words Color = (X ^ Y). This Way you get a nice looking Texture like this!

Conclusion

In this article we setup our window for mode 13h and implemented a double buffer with VRETRACE Signal for the upcoming project. In the next article we will try to load some bmp images into our buffer.