Realmode Assembly - Writing bootable stuff - Part 5

Realmode Assembly - Writing bootable stuff

Part 5: Graphic Mode


What is this?

This is going to be a walk-through in writing Operation Systems in assembly which operate purely in Realmode.
Goal will be writing different kernels from a simple “Hello World” over small terminal programs to
graphically displayed games.

I decided to split this walk-through into small parts so those few who are interested in this have
enough time to read the necessary theory and references before getting code smashed in the face,
I hope this prevents the parts from getting unnecessary long or confusing and it also gives
me the time to properly check the information and code I provide (although errors might sneak in
which makes reading the sources for reliable information the recommended way if you are really interested in how this works)

Requirements

  • Understanding x86 assembly
  • Reading the previous articles

Notes

  • This information is the result of my research and own programming, everything said here might be wrong, correct me if you spot mistakes though!
  • I will try to list my sources at the bottom but I can’t guarantee that these are all of them.
  • I’M NOT RESPONSIBLE IF YOU BREAK SOMETHING USING INFORMATION I PROVIDED HERE.

Content of this Article

This article is about rendering pixel, shapes and images in real mode using BIOS interrupts.

Look into the git repository if you are interested in details not covered in this article :slight_smile:


How to render in real mode

You might have asked yourself how the BIOS was able to render text as shown in the previous parts.
Well the answer to that is also the answer of how to render more custom images/shapes (as it uses the same interrupt routines):
After the system BIOS (the one installed on the motherboard) is done with its basic initializing it starts loading the Video BIOS (installed on the graphic card/chip set) which then handles interaction screen interactions through BIOS routines.

By default the Video BIOS starts in a Black/White mode with 40 characters for 25 lines (Mode 00).

As you might remember the interrupt to print characters we used (interrupt 0x10,0xE) had a color attribute not usable for us up to now because of the color restriction.

|----int----|--ah--|------------al------------|---------bh-------|-------bl-------|-----------Description-----------|
|  int 0x10 | 0xE  | ASCII Character to print | Page to write to | Color Attribute| Print a character to the screen |
|-----------|------|--------------------------|------------------|----------------|---------------------------------|

By changing the display mode to Mode 01 we would get access to 16 colors with the same 40 characters for 25 lines configuration and could actually specify colors for text output.

But as you might have guessed we won’t use a text mode (like Mode 00 or 01) but a mode with graphic output.
There are a few available and all of them have unique characteristics but from my experience Mode 0x13 is the easiest to use by both interrupts and direct memory access and it provides a good screen size of 320x200 pixel and can display 256 colors at the same time (color palette can be changed through interrupts).
Reason for it being easy to use is that unlike other VGA modes it doesn’t use a rather complex reference mode but is accessible as a plain memory with each byte mapped to a pixel on screen.

(More information about the specifications of the BIOS display modes within the “VGABIOS.TXT” and “INTERRUP.TXT”)
(More information about how the different graphic modes work at the “OSDevVga” resource)

We can switch graphic modes by using interrupt 0x10, 0x0 and setting al to the display mode we want.

|----int----|--ah--|-----------------al-------------------|-----------Description-----------|
|  int 0x10 | 0x13 | Number representing new Display Mode | Sets display mode               |
|-----------|------|--------------------------------------|---------------------------------|

If we are in a graphic mode (not a text mode) we can use the interrupt 0x10, 0xC to draw pixels.

|----int----|--ah--|--------------al---------------|---------bh-------|-----cx-----|-----dx-----|----------Description------------|
|  int 0x10 | 0xC  | Index of color within palette | Page to write to | x-position | y-position | Draws a pixel at given point    |
|-----------|------|-------------------------------|------------------|------------|------------|---------------------------------|

As I already said we can also write pixels by writing directly to RAM which is significantly faster especially within an emulator but the method to write and the address to write to change for each graphic mode which makes it quite complex to generalize. We will look into that in a later article (but it will be limited to graphic mode 0x13).

Also note that we can still print text within a graphic mode which is really useful for debugging purpose.

OK, so we will use display mode 0x13 so let’s switch to it:

mov ah, 0   ;Set display mode
mov al, 13h ;13h = 320x200, 256 colors
int  0x10   ;Video BIOS Services

Drawing squares

So we know that we can draw single pixels through interrupt 0x10, 0xC but how about more complex shapes?
Let’s start simple with a function to draw a square:

Drawing a square consist of iterating through a x-axis and a y-axis and drawing a pixel for each position.

;------------------------------------------------------
;cx = xpos , dx = ypos, si = x-length, di = y-length, al = color
drawBox:
	push si               ;save x-length
	.for_x:
		push di           ;save y-length
		.for_y:
			pusha
			mov bh, 0     ;page number (0 is default)
			add cx, si    ;cx = x-coordinate
			add dx, di    ;dx = y-coordinate
			mov ah, 0xC   ;write pixel at coordinate
			int 0x10      ;draw pixel!
			popa
		sub di, 1         ;decrease di by one and set flags
		jnz .for_y        ;repeat for y-length times
		pop di            ;restore di to y-length
	sub si, 1             ;decrease si by one and set flags
	jnz .for_x            ;repeat for x-length times
	pop si                ;restore si to x-length  -> starting state restored
	ret
;------------------------------------------------------

Drawing circles

OK, drawing a square was quite simple but how about a circle?

Drawing circles works by iterating through x- and y-axis and then only drawing the pixels with a distance from the middle point lower or equal to the radius.

	x, y = position of pixel
	middle = middle of circle
	distance = sqrt((x-middle.x)^2 + (y-middle.y)^2)
	if distance <= radius:
		draw pixel

Now you might notice the square root function within the pseudo code and although using the FPU(FSQRT) would make that possible a more cheaty approach is way simpler:

		if distance <= radius:
			draw pixel

  <=> 	if distance^2 <= radius^2:
			draw pixel

  <=>  	distanceNSQRT = (x-middle.x)^2 + (y-middle.y)^2
		if distanceNSQRT <= radius^2:
			draw pixel
;------------------------------------------------------
;cx = xpos , dx = ypos, si = radius, al = color
drawCircle:
	pusha                      ;save all registers
	mov di, si
	add di, si                 ;di = si * 2, di = diameter y-axis
	mov bp, si                 ;bp = copy of radius
	add si, si                 ;si = si * 2, si = diameter x-axis
	.for_x:
		push di                ;save y-length
		.for_y:
			pusha
			add cx, si         ;cx = x-coordinate (start x + si)
			add dx, di         ;dx = y-coordinate (start y + di)
                               ;(x-middle.x)^2 + (y-middle.y)^2 <= radius^2
			                   ;(si-bp)^2 + (di-bp)^2 <= bp^2
			sub si, bp         ;di = y - r
			sub di, bp         ;di = x - r
			imul si, si        ;si = x^2
			imul di, di        ;di = y^2
			add si, di         ;add (x-r)^2 and (y-r)^2
			imul bp, bp        ;signed multiplication, r * r = r^2
			cmp si, bp         ;if r^2 >= distance^2: point is within circle
			jg .skip           ;if greater: point is not within circle
			mov bh, 0          ;page number (0 is default)
			mov ah, 0xC        ;write pixel at coordinate
			int 0x10           ;draw pixel!
			.skip:
			popa
		sub di, 1              ;decrease di by one and set flags
		jnz .for_y             ;repeat for y-length
		pop di                 ;restore di
	sub si, 1                  ;decrease si by one and set flags
	jnz .for_x                 ;repeat for x-length
	popa                       ;restore all registers
	ret
;------------------------------------------------------

Drawing an image

By default the color indexes of interrupt 0x10, 0xC refer to the VGA 256-color palette and although those are changeable through interrupts we can see already see that 256 colors are quite a bit (enough to make the color black appear 10 times in it).

Let’s try to encode a small image into those indexes and display it!

#this python script is quite slow for larger images >_<
from PIL import Image                                         #Import Image from Pillow

paletteFile = "colors.png"                                    #palette the BIOS uses
convertFile = "fox.png"                                       #image to turn into a binary
outputFile  = "image.bin"                                     #name of output file

pal = Image.open(paletteFile).convert('RGB')
palette = pal.load()                                          #load pixels of the palette
image = Image.open(convertFile).convert('RGB')
pixels = image.load()                                         #load pixels of the image

binary = open(outputFile, "wb")                               #clear/create binary file

list = []                                                     #create a list for the palette
for y in range(pal.height):
    for x in range(pal.width):
        list.append(palette[x,y])                             #save the palette into an array

binary.write(bytearray([image.width&0xFF,image.height&0xFF])) #write width and height as the first two bytes for variable image size
data = []
for x in range(image.width):
    x = image.width - x - 1                                   #invert x-axis (for shorter assembly code)
    for y in range(image.height):
        y = image.height - y - 1                              #invert y-axis (for shorter assembly code)
        difference = 0xFFFFFFF                                #init difference with a high value
        choice = 0                                            #the index of the color nearest to the original pixel color
        index = 0                                             #current index within the palette array
        for c in list:
            dif = sum([(pixels[x,y][i] - c[i])**2 for i in range(3)]) #calculate difference for RGB values
            if dif < difference:
                difference = dif
                choice = index
            index += 1
        binary.write(bytearray([choice&0xFF]))                #write nearest palette index into binary file as 1 byte
binary.close()                                                #close file handle
print("Done.")
;------------------------------------------------------
;si = image source
drawImage:
	pusha
	xor ax, ax
	lodsb
	mov cx, ax                      ;x-length (first byte of binary)
	lodsb
	mov dx, ax                      ;y-length (2nd byte of binary)
	.for_x:
		push dx
		.for_y:
			mov bh, 0               ;page number (0 is default)
			lodsb                   ;read byte to al (color) -> next byte
			mov ah, 0xC             ;write pixel at coordinate (cx, dx)
			int 0x10                ;draw!
		sub dx, 1                   ;decrease dx by one and set flags
		jnz .for_y                  ;repeat for y-length
		pop dx                      ;restore dx (y-length)
	sub cx, 1                       ;decrease si by one and set flags
	jnz .for_x                      ;repeat for x-length
	popa                            ;restore everything
	ret
;------------------------------------------------------

imageFile: incbin "image.bin"       ;include the raw image binary into the kernel

As you might notice the script tries to find the most fitting palette color for a given pixel, this isn’t lossless and thus changes the image appearance and quality (obviously considering we just encoded 3 bytes (RGB8) into 1 byte (index within a palette)).
We will look into ways to load our own palettes in a later part to display colors correctly (at least if you don’t intent to display more than 256 unique colors at the same time).

Problems with rendering using interrupts

As of now we’ve only rendered static images and everything was fine but for something like a game we would want our screen to be more dynamic and render different things each frame.
As we don’t want previous frames to overlap with our current one we will need to clear the screen before rending the next frame and that’s where stuff goes wrong.

*No code included as it’s nothing new really, look into the git repository if you are interested

As you might see in this video the movement of the square is slow and sometimes the screen flickers black.
That is because clearing the screen (drawing a black box that covers the screen) is heavy on the emulator and slows it down remarkable.
You most likely won’t notice this on real hardware as the interrupts are processed way faster but this throws up the problem of timing and synchronization. For example long render time could mess up game logic processing or cause the game to run on different tick rates depending on hardware (which isn’t good).
And now this annoying flacking (it will also happen on real hardware): The screen refreshes at a specific rate and timing and because our rendering code is not synchronized to that the screen sometimes gets displayed even though the rendering code isn’t fully executed yet.

Those and a few other problems (and how to solve them) will be the topic in the next few parts!

Conclusion

This concludes the rendering through interrupts part.
I hope I was able to show how to use the draw pixel interrupt and how to actually make use of it.
The next and last few parts will consist of using the knowledge from the previous parts (and additional information) to write a small game (I’ve not yet decided what kind of game to write ,I tend to some jump&run mario thing, but I’m open for suggestions).
If I wrote something wrong just point it out and I will correct it also feedback is appreciated.

Next Part

Previous Part


Link to source files:

Sources and References

24 Likes

I’m not gunna lie, this might be the best quality post we’ve had on 0x00sec to date.

Very well done @Leeky

5 Likes