Writing text

Writing text on the graphics screen is absolutely necerrary. You obviously want to display some textual information even if you write a sprite based game. For showing a text properly, we need characters. On a graphical screen there is no predefined dimension for a character (while on character screens there is), therefore it's really up to us, what we are going to implement. For example, in the VGA rom the usual character sizes are 8x8, 8x14 and 8x16 pixels, but we don't have to use these fonts.
As a matter of fact, these aren't look well on a screen with small resolution. In this example, I am going to use my own defined small fonts with the size 6x7. This set contains only the capital letters, numbers and signs, but it's enough to present the method. This is how they look like:

The caracters are just like images. The main difference is that in this case we don't have to store the color information for each pixel, usually we write out the text with a selected color (you can call it INK color if you want to). This means, that we don't need bytes for storing the individual pixels, bits are enough. Have a look at bitmap of the letter "A":

The red box shows the character actual size, while the blue one shows, how it looks like if we use one byte (8 bits) for each pixel in a row. In this way one character bitmap needs 7 bytes. The bitmap definition is quite simple: we have bit "1" where we have pixels, "0"s otherwise.

Binary dataHex. data

Show one char

How are we going to show a string? He have to show each characters one by one. So how are we going to show one character? Consider the bitmap is a monochrom image. Now the required steps are

{ input: ES:di - points to the screen
            al - charcode
            dl - color }
procedure putchar; assembler;
   push si              { save registers }
   push di
   push cx

   sub  al,32           { we don't have the 1st 32 ascii chars }
   mov  ah,7
   mul  ah              { character bitmap offset = 7*charcode }

   lea  si,chr6x7_dat
   add  si,ax           { SI points to the bitmap }

   mov  cl,7            { LOOP #0 }
   segCS lodsb          { next byte from the bitmap }

   mov  ch,6            { LOOP #1 }
   shl  al,1            { shift out 1 bit }
   jnc  @skip
   mov  ES:[di],dl      { show the pixel }
   inc  di
   dec  ch              { all pixels in a line, close LOOP#1 }
   jnz  @l1
   add  di,320-6        { next line }
   dec  cl
   jnz  @l0             { all lines, close LOOP#0 }

   pop  cx              { load registers back from the stack }
   pop  di
   pop  si
   retn                 { near return }
Well. It takes some instructions, but it's not as complex as it seems. Assume that ES:DI contains the screen address (DESTADDR), AL has the ASCII code, while DL has the INK color. At the beginning we save the used registers (SI,DI,CX) as we'll need them outside the procedure. Then we caluclate the address for the chararcter. Because we have bitmaps only for the ASCII codeset 32..95, we have to sub 32 from the code (a simple offset). Then a multiply by 7 gives the offset. The predefined label chr6x7_dat is the address for the bitmap.

Now a really important detail: we store the bitmap data in the same code segment where the putchar procedure is. This means that the bitmap is included into the unit GFX256, we don't need to read it from a file. Storing data in the code segment is not a nice thing, you would say, but it makes our life easier. Plus, in this case - as later you'll see - there is three different memory area what we must deal with, that's why I prefer storing the bitmaps inside the code segment.
(If you want to see, how it looks like, click here. Not too exciting though.)

The instruction SEGCS LODSB loads the next byte from the bitmap. Then the internal loop checks all the bits. I use the SHL instruction so I shift bits left, then if the bit is one, the flag Carry will be 1. If this is one, we show the pixel, otherwise just skip that location.

At the end of the procedure we load back the saved registers from the stack and return. Although the assembly procedure frame contain a ret intruction anyway, I use the RETN here, becuase I am going to call this function locally from this unit later. (It means, it's a local function and will not be available from outside.)

Show string

Shoing a string now will be pretty strightforward. A pascal string is not too complicated. It has maximum 256 bytes. The first byte always contains the number of characters, and the rest of it is just the string itself.

procedure print(x,y,color:integer; const s:string); assembler;
   mov  ES,SegA000      { Screen segment }
   mov  ax,320
   mul  y
   add  ax,x
   mov  di,ax           { Address calculation, store the result in DI }

   mov  dl,color.byte   { color to the DL }
   mov  bx,DS           { save DS         }
   lds  si,s            { source string   }
   mov  cl,al           { 1st byte : length }
   xor  ch,ch
   jcxz @quit           { quit if it's an empty string }

   lodsb                { get next charcode }
   call near ptr putchar{ show the char - near procedure call }
   add  di,6            { next screen position }
   loop @chrloop        { show all chars  }

   mov  DS,bx           { restore DS      }
Okay, do it then. Steps....
  • Calculate the screen address (ES:DI)
  • Load the pointer that points to the string (DS:SI)
  • Check the 1st byte, this is the length of the string. If zero, it's an empty string.
  • Do a loop that shows the chars one by one
Note, that before I load the pointer into DS:SI, I save the DS in BX. This is because the Pascal requires the DS to be constant. (It points to the data segment of the main program.)


program prtstr;

uses gfx256;

 i : integer;
 s : string;

 for i:=1 to 15 do
  print(10+i*8,3+(i-1)*8,i,'HELLO WORLD!');

 for i:=32 to 63 do s:=s+chr(i);

 for i:=64 to 95 do s:=s+chr(i);

 print(110,160, 4,'WHAT DO YOU KNOW?');
 print(111,160,15,'WHAT DO YOU KNOW?');

The result as we see on the screen:

Downloads: [ Gfx256 unit | Print string ]