In the late 80's and early 90's, when Real Mode was still in widespread use, the most common piece of graphics hardware found on a PC was the VGA Chip. To this day, practically every single GPU implements its functionality, and it is supported by most emulators, including QEMU. The card offers a variety of modes, but the most popular of them was the well-known Mode 13h.

Mode 13h

Due to its relative ease of use through BIOS calls when in Real Mode, Mode 13h was chosen as the graphics mode for Kapow. It is easy to set by calling a BIOS interrupt in the following way:

mov ax, 0x13
int 0x10

As can be seen here, Mode 13h gets its name from the value put into ax to set it, 13 hexadecimal.

Drawing

Once this is done, the screen can be written to as a linear buffer, in the segment 0xA0000. The screen is sized at 320x200 pixels, taking up an entire segment of memory (64 kilobytes). Each byte represents a single pixel, the value of which is an index in the palette, which will be discussed later.

Since this is a linear framebuffer, the following equation could be used in order to draw a pixel at a certain x and y in the screen.

$$o = x+y*320$$

Where $$o$$ is the offset from 0xA0000 and $$x$$,$$y$$ are your desired coordinates.

From this equation, a function like this could be used in order to write a single pixel to a desired location:

; Draws a pixel on the screen
; al - color
; bx - x-coordinate
; cx - y-coordinate
put_pixel:
push bp
mov bp, sp

push ax
mov ax, 320
mul cx
pop ax

mov dx, 0xA000 ; This is used to set the segment register, so only the last 4 bits are of use.
mov gs, dx
mov [gs:bx], al

mov sp, bp
pop bp
ret

The Palette

As you might imagine, given that we are using one byte per pixel, that greatly limits the amount of color information we can use. If we were to use an RGB representation, for example, we would only have around 2.5 bits per component.

In order to solve this, Mode 13h operates in something called indexed color mode, where the value of each byte represents not color information directly, but instead an index in an array of colors. By default, the BIOS loads some colors into the VGA chip, but if we want to use artwork of any quality, it is a good idea to customize the colors to our liking.

Setting a color in the palette involves direct communication with the VGA card. This might sound complicated but in reality is quite simple. Kapow implements this in the following way:

; Sets an index in a palette to a color
; ah - index
; bl - red (0-63)
; bh - green (0-63)
; cl - blue (0-63)
set_palette_index:
push bp
mov bp, sp

mov al, 0xFF
mov dx, 0x3C6           ; PEL Mask Register
out dx, al              ; Prepare VGA card for color change

mov al, ah
mov dx, 0x3C8           ; PEL Address Write Mode Register
out dx, al              ; Send what color index to write to

mov dx, 0x3C9           ; PEL Data Register

mov al, bl
out dx, al

mov al, bh
out dx, al

mov al, cl
out dx, al

mov sp, bp
pop bp
ret

It is worth noting that when uploading colors into the palette, the VGA card only cares about the first 6 bits of the byte giving, discarding the last two. That is why their value is denoted as 0-63.

So for example, if we were to set index 163 to a color of our liking, whenever 163 is encountered in a byte in our framebuffer, it would be drawn as the color we specified.

VSync

If you were to directly write to the framebuffer and had moving elements, you would notice severe screen tearing. This can be solved by buffering the contents of your screen before writing it to the VGA chip, and synchronizing it to your to the video signal, to ensure no writing is done while screen is being refreshed.

To do this, you must first write the screen's contents to another segment, for example 0x70000, instead of directly to 0xA0000. Then, you can copy the contents of the temporary buffer to the actual framebuffer.

But before you write to the VGA's memory, you must make sure that the screen is not currently being refreshed. This can be done by querying the VGA card, and waiting if this is the case. This can be done in the following way:

vt_set:
in al, dx
and al, 8
jnz vt_set

vt_clr:
in al, dx
and al, 8
jz vt_clr

After that, to perform the copying, a routine similar to this one could be used:

copy_buffer:
push bp
mov bp, sp

mov cx, 0x7000
mov ds, cx
xor si, si             ;ds:si = source

mov cx, 0xa000
mov es, cx
xor di, di             ;es:di = destination

mov cx, 32000           ;32k words to fill the screen

mov dx, 0x3da           ; VGA status register

rep movsw

mov sp, bp
pop bp
ret

Kapow's Renderer

Drawing Sprites

In a game, it is very important to be able to draw sprites. One might even say, it is imperative. In order to draw a sprite, Kapow iterates through the sprite data and places pixel by pixel in the correct area. The value 0xFF is taken as a transparent pixel and thus this value is not written. The code for this is as follows:

; Draws a sprite
; ax - y-position
; bx - x-position
; ch - width
; cl - height
; dx - sprite location
draw_sprite:
push bp
mov bp, sp

push dx ; Save sprite location
mov dx, screen_width
mul dx ; Multiply y position with screen width (320), to get offset
add bx, ax ; Add offset to x, to get absolute start location

mov ax, vga_mem
mov es, ax
mov ax, sprite_seg
mov gs, ax ; Set up segments where transfers will occur. From sprite_seg to vga_mem

pop ax ; Put saved sprite location in ax
xor dx, dx
mov dl, cl ; Move height to dl

shr cx, 8 ; Shift cx right 8 places, effcetively putting width in cl

push cx ; Save width
ds1:
push ax ; Save sprite location + offset
push bx ; Save current offset position
mov bx, ax ; Move sprite location to bx
mov al, [gs:bx] ; Move sprite's pixel value to al
pop bx ; Reload bx with offset positon
cmp al, 0xFF ; Check if pixel should be transparent
je dssk ; If so, don't place the pixel
mov [es:bx], al
dssk:
pop ax ; Restore sprite location + offset
inc bx ; increase offset in gfx buffer
inc ax ; Increase sprite offset

loop ds1 ; Continue looping until done with this row of pixels in sprite
dec dx ; One less row to go
cmp dx, 0 ; Check if all rows done
jz dsd ; Jump if done
pop cx ; Restore sprite width
push cx ; Save sprite width
add bx, screen_width ; Add screen width to the offset, going to the next row
sub bx, cx ; Subtract sprite width from bx, to arrive at the correct column

jmp ds1

dsd:    pop cx

mov sp, bp
pop bp
ret

Drawing Animations

How animations are drawn in Kapow will be explained in more detail in a later post.