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:
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. |