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.