Search (using Google):  Web Karig

 

14 March 2004

Testing the font

I said in my previous entry that I'd come up with a font that I wanted to use to print text to the screen in graphics mode.

Layouts

Before I can write code to print a font on the screen, I have to know the layout of video memory, and I have to select a layout for my font.

Font layout

To review: The layout of the font is simple. Each character image, or "glyph," consists of 16 consecutive bytes (128 bits) of color data. Each glyph is printed to a screen slot eight pixels wide and sixteen pixels high. Each of the sixteen bytes represents one of the sixteen pixel rows, and each bit in a byte represents one of the eight pixels in a row. The first of the sixteen bytes represents the top pixel row; each subsequent byte represents the next row down. The first, or lowest, bit in a byte (bit 0) represents the leftmost pixel in the row; each subsequent bit represents the next pixel to the right.

Each bit represents the color of a pixel. A one bit will be printed as a background-color (white) pixel; a zero bit will be printed as a foreground-color (black) pixel. (A space character consists of 128 bits set to one.)

This font layout is probably the best layout I could come up with. It uses relatively little memory (only 4KB for 256 character glyphs); finding a particular glyph is simple (you just calculate its address); and generating colored pixels from the font bits is relatively straightforward, so printing text to the screen using this font should always be blindingly fast.

Video memory

The graphics screen is arranged into scanlines. Each scanline on the screen is a single row of pixels running all the way across the screen. If the current resolution is 640x480, then each scanline contains 640 pixels, and the screen contains 480 scanlines.

The video memory is arranged similarly — as an array of contiguous buffers, each representing all the pixels in a particular scanline. Each of these arrays is large enough to hold data representing the 640 pixels in a scanline, and there are 480 of these arrays. The first of these arrays represents the scanline running along the top of the screen; the second represents the scanline right below that; and so on.

Karig uses 16-bit color, so each video memory array devotes sixteen bits — two bytes — to each pixel. Thus each scanline array in video memory is 640 * 2 or 1280 bytes long, and each scanline array begins 1280 bytes after the beginning of the previous one. Thus:

  • Given two pixels side by side on the screen: the address in video memory of the right pixel's color is one word (2 bytes) greater than the address of the left pixel's color.
  • Given two pixels one immediately above the other: the address in video memory of the bottom pixel's color is one scanline — 640 words (1280 bytes) — greater than the address of the top pixel.

How a character is printed

My boot sector prints a message by passing the ASCII value of each character in the message to next_glyph. (My code also has to pass in EDI an address within the screen buffer to which the pixel-color words are to be saved. Next_glyph saves this address and will use it later.)

next_glyph:
; Prints character whose ASCII value is passed in AL (EAX)
; PASS eax=character, edi=pointer into screen buffer
		push	edi

Figuring out what glyph to use

Next_glyph converts the ASCII value into an address within the font.

		and	eax, 0xFF
		shl	eax, 4    ; multiply by 16 (# bytes in font per char)
		add	eax, font ; create offset into font
		mov	esi, eax

Grabbing a registerful of font data

Next_glyph calls next_4_scanlines, which grabs four bytes of font data.

next_4_scanlines:
; Converts 32 bits in EAX to 4 rows of 8 pixels on screen
; PASS esi=pointer into font, edi=pointer into screen buffer
		mov	eax, [esi]   ; grab 4 bytes from font

Translating font bits into screen words

The 32 bits of font data in EAX constitute four 8-bit "scanlines" or rows of pixels. To handle each scanline, next_4_scanlines calls next_scanline.

In turn, each 8-bit "scanline" consists of four 2-bit pairs of pixels, so next_scanline calls next_2_pixels. I break a scanline into pixel pairs, because EAX can hold 32 bits, and a pair of pixel-color words is 32 bits, so I decided to use each pixel pair as an offset into a table of colors. So next_2_pixels uses and disposes of the bottom two bits in EAX like this:

next_2_pixels:
; Converts low 2 bits in EAX to 2 pixels on screen
; PASS eax=bits from font, edi=pointer into screen buffer
		mov	ebx, eax
		and	ebx, 0x03 ; leave only bits 0..1 (value 0..3)
		mov	ebx, [colors+ebx*4]
		shr	eax, 2

Here's where things can get confusing. The colors table looks like this:

black equ 0x0000
white equ 0xFFFF

colors:
		dw	black, black ; entry 00b
		dw	white, black ; entry 01b
		dw	black, white ; entry 10b
		dw	white, white ; entry 11b

You might be wondering: If a zero bit shows up on the screen as black, and a one bit shows up as white, then if entry 00b is "black, black" and entry 11b is "white, white," then shouldn't entry 01b should be "black, white," not "white, black"?

Remember how bits are laid out in memory on x86 machines. In memory, bit 0 comes first, and bits 1, 2, 3, etc. follow in order. The binary numbers here (00b, 01b, 10b, 11b) actually show the bits backward, with bit 0 in the rightmost position; they really should be 00b, 10b, 01b, 11b to reflect bits in registers and in memory accurately. So entry 01b above should be "white, black."

(I found out the hard way. If the two middle entries above are switched, the characters printed on the screen will be jumbled and unrecognizable.)

Printing a row of eight pixels

Next_2_pixels, once it has the two colors in EAX to print to the screen, saves them to the video screen buffer, advances the video pointer by the size of two color pixels, and returns to next_scanline.

		mov	[edi], ebx
		lea	edi, [edi+4]
		ret

Next_scanline calls next_2_pixels four times to print the eight pixels that constitute a complete pixel row. At this point, the pointer into video memory points 16 bytes ahead of where it did when next_scanline was called, and we now need to have the pointer point to the pixel directly below the first pixel in the row just drawn. So the pointer needs to be advanced by the size of a scanline (640 * 2) minus the size of a pixel row (16) before next_scanline returns to next_4_scanlines.

Here is the complete routine:

next_scanline:
; Converts low 8 bits in EAX to 8 pixels on screen
; PASS eax=bits from font, edi=pointer into screen buffer
		call	next_2_pixels
		call	next_2_pixels
		call	next_2_pixels
		call	next_2_pixels
		add	edi, 640*2-16
		ret

Starting the next row of pixels

Next_4_scanlines calls next_scanline four times to take care of the 32 bits (four 8-bit "scanlines" or pixel rows) in EAX, then advances the pointer into the font data to the next four bytes.

Here is the complete routine. It grabs four bytes and "prints" them, then positions the pointer so that when the routine is called again, it will grab the next four bytes. Thus it can simply be called four times in a row to print sixteen scanlines.

next_4_scanlines:
; Converts 32 bits in EAX to 4 rows of 8 pixels on screen
; PASS esi=pointer into font, edi=pointer into screen buffer
		mov	eax, [esi]   ; grab 4 bytes from font
		call	next_scanline
		call	next_scanline
		call	next_scanline
		call	next_scanline
		lea	esi, [esi+4] ; move to next 4 bytes in font
		ret

Next_glyph calls next_4_scanlines four times.

		call	next_4_scanlines
		call	next_4_scanlines
		call	next_4_scanlines
		call	next_4_scanlines

Preparing for the next glyph

At this point the glyph has been printed on the screen. EDI points to the first pixel in the pixel row below the glyph; it needs to point to the first pixel in the pixel row to the right of the top pixel row in the glyph. So the original EDI is retrieved from the stack and advanced by the size of a pixel row, and the code is ready to start printing the next glyph.

		pop	edi
		add	edi, 16   ; width in bytes of one character
		ret

Printing a message on a white screen

The code to make the screen white looks like this:

; ------ Fill screen buffer with a color.
		mov	eax, 0xFFFFFFFF ; white
		mov	edi, buffer
		mov	ecx, 640*480/2
		rep	stosd

The loop to print each character in a message looks like this:

; ------ Print message using the font.
		mov	edi, buffer
		xor	ecx, ecx
	.m:	mov	al, [msg1+ecx]
		call	next_glyph
		inc	ecx
		cmp	ecx, len1
		jl	.m

After the message is printed to the buffer, the buffer is copied into the memory on the video card:

; ------ Copy screen buffer into video memory.
		mov	esi, buffer
		mov	edi, 0xE0000000
		mov	ecx, 640*480/2
		rep	movsd

The message is:

msg1:		db	"This is the system font that I will be using in Karig."
len1:		equ	$-msg1

Screenshot

The result of the code is my first-ever screenshot. I'm so proud. :-)

Source code and font data

Here is the source code. If you have both NASM for Windows and RawWriteWin either in the current directory or in your path, you can just stick a floppy disk into the drive and run "make.bat" from the command line. Then reboot the computer and wait for the message to appear on the screen in a nice fun chunky arcade-graphics font. :-)

The ZIP file also includes the font2bin program and its source code. You can use it if you want, but it does no error checking, so it'll just crash if things aren't exactly right. (See my previous entry about all the assumptions it makes.) You can open the font.bmp file in Microsoft Paint, change the appearance of the characters (using only black and white pixels, remember), rerun font2bin to regenerate font.bin, rerun make.bat, and see the message in your own font. Enjoy!

Check the index for other entries.