Text in a hurry!

Table of Contents:
Speeeeeeeeeeedddd!!!
Stuff
Optimizing WriteChar
Plan of attack
Fast fonts
An example program


Speeeeeeeeeeedddd!!!

Our ultimate goal: A text routine that is flexible and throws words to the screen in a hurry. So far we have a flexible routine for doing 1 bit per pixel fonts that is kinda fast... just not nearly fast enough!


Stuff

So far, we have a routine that has these loops all in one procedure, like this:

PROCEDURE WriteG();
BEGIN
  FOR EachCharacterInString DO
    FOR EachLineInCharacter DO
      FOR EachPixelInLine DO
        IF Pixel THEN SetPixel(Pixel);
END;
Although this works, it is sort of limiting. Because of a number of reasons that will become apparent, we'll split this up like this:

PROCEDURE WriteChar();
BEGIN
  FOR EachLineInCharacter DO
    FOR EachPixelInLine DO
      IF Pixel THEN SetPixel(Pixel);
END;

PROCEDURE WriteG();
BEGIN
  FOR EachCharacterInString DO
    DrawLetter;
END;
One of the reasons that this is important to do is because it lets us focus on the optimization of the critical WriteChar procedure instead of optimizing (unneccesarily) the entire thing. Anyways, as you probably could guess, that is exactly what we're about to do!


Optimizing WriteChar

Okay, here we are with a slow font routine... what shall we do. The first thing that comes to my mind (and should come to yours) is to optimize the inner loops of the routine. Those inner loops correlate to the DrawLetter routine. Here is the main chunk of code that comes from the DrawLetter routine. This is the target of our optimization:

IF (C <= Font.LastChar) AND (C >= Font.FirstChar) THEN  { Range check   }
BEGIN
  FontPtr := (C-Font.FirstChar)*Font.CharSize;   { Generate ptr to data }
  BitsLeft := 0;
  FOR Yc := Y TO Y+Font.Height-1 DO              { Step over each pixel }
  BEGIN                                          { in the Y direction   }
    FOR Xc := X TO X+Font.Width-1 DO             { Step over each pixel }
    BEGIN                                        { In the X direction   }
      IF BitsLeft = 0 THEN
      BEGIN
        FontData := Font.Data^[FontPtr];         { Refill FontData when }
        INC(FontPtr);                            { it gets empty        }
        BitsLeft := 8;
      END;
      IF (FontData AND 128) = 128 THEN           { Draw a pixel?        }
        SetPixel(Xc, Yc, Color)
      ELSE IF (Font.Background > 0) THEN         { Draw the background? }
        SetPixel(Xc, Yc, Font.Background);

      FontData := FontData SHL 1;                { Next bit please!     }
      DEC(BitsLeft);
    END;
  END;
END;
When I look at this code, I can see a number of things can can be drastically sped up:

  • The first big one is the fact that the memory location of each pixel is calculated for each pixel in the SetPixel routine even though the pixels are all right next to each other. If we bypass the SetPixel routine, we can improve performance a lot.

  • Accessing Font.Data^ the way that we do is very ineffecient. Turbo Pascal has to reload the pointer into ES:DI every time the array is read.

  • In assembly language, we can test the high bit of the data byte with the same instruction that we shift it with. Which simplifies and speeds up the routine.


    Plan of attack

    In order to keep the routine fully functional and take advantage of these observations, here is my plan of attack. The registers will look like this in the main routine:

    AL = Foreground Color
    AH = Font.Background
    BX = Screen.Width - Font.Width
    CL = X Counter
    CH = Y Counter
    DL = Bit Counter
    DH = Font Data
    
    DS:SI = Font.Data^
    ES:DI = Screen Pointer
    
    To sum this up: AL holds the current color that the text is being drawn in, and AH holds the current background color of the font, taken from the Font structure. BX holds distance in memory locations from the right most pixel in the current line to the first pixel in the next line of the font... To illustrate:

    BX holds the distance covered by the large white lines. CL holds the variable named Xc above, and CH holds the Yc variable. DL holds the number of bits held in the DH register. The DH register holds the data read from the font. The registers ES:DI holds the pointer to the current pixel on the screen. The DS:SI pair point to the data in the font.

    All of this leads up to thing... the actual routine.


    Fast fonts

    Here is the optimized version of the DrawLetter routine:

    PROCEDURE DrawLetter1(Font : FontType; X, Y : INTEGER; C : CHAR; Color : BYTE);
    BEGIN
    ASM
      mov AL, C                  { Range check }
      cmp AL, Font.LastChar      { IF (C <= Font.LastChar) AND        }
      ja @Exit
      sub AL, Font.FirstChar     {  (C >= Font.FirstChar) THEN        }
      js @Exit                   { Generate ptr to data               }
    
      mul Font.CharSize          { (C-Font.FirstChar)*Font.CharSize   }
      add AX, WORD [Font.Data]   { OFFSET Font.Data^                  }
      mov SI, AX
    
      les DI, Screen.Buffer      { Calculate the address of the first }
      add DI, X                  { pixel of the letter.               }
      mov BX, Y
      add BX, BX
      add DI, DS:[BX+OFFSET Screen.YTable]
    
      mov AL, Color
      mov AH, Font.Background
      mov BX, Screen.Width
      mov CL, Font.Width    { FOR Xc := X TO X+Font.Width-1 DO   }
      mov CH, 0
      sub BX, CX            { BX = Screen.Width-Font.Width }
    
      mov CH, Font.Height   { FOR Yc := Y TO Y+Font.Height-1 DO  }
      mov DL, 0             { BitsLeft := 0; }
    
      push DS                    { We must save DS!              }
      mov DS, WORD [Font.Data+2] { DS = Seg(Font.Data^)          }
    @Loop:
      or DL, DL
      jnz @GotMoreBits      { IF BitsLeft = 0 THEN               }
      mov DH, DS:[SI]       {   FontData := Font.Data^[FontPtr]; }
      inc SI                {   INC(FontPtr);                    }
      mov DL, 8             {   BitsLeft := 8;                   }
    @GotMoreBits:           { END;                               }
    
      dec DL                { DEC(BitsLeft);                     }
      shl DH, 1             { FontData := FontData SHL 1;        }
      jnc @NoPixel          { IF (FontData AND 128) = 128 THEN   }
      mov ES:[DI], AL       {   SetPixel(Xc, Yc, Color)          }
      jmp @NextLoop
    
    @NoPixel:
      or AH, AH             { ELSE IF (Font.Background > 0) THEN }
      jz @NextLoop
      mov ES:[DI], AH       {   SetPixel(Xc, Yc, Font.Background); }
    @NextLoop:
      inc DI
      dec CL
      jnz @Loop
    
      mov CL, Font.Width     { Reload X counter     }
      add DI, BX             { Move DI to next line }
      dec CH
      jnz @Loop
      pop DS
    @Exit:
    END;
    END;
    
    Now why would I do this? What point is there in breaking up the routine into two seperate routines? The answer lies in the fact that we might want to clip this text at some point. There, we will find that there are three distinct cases that can happen with a character:

    • The letter requires no clipping.
    • The letter only requires clipping on the top or bottom.
    • The letter requires any other combination of top, bottom, left, or right clipping.

    These three cases are sorted from the fastest to slowest. If we take the DrawLetter routine out of the WriteG procedure, we can call it from both the WriteG procedure and the WriteGC (WriteG Clipped) procedure. This makes it more flexible. In the next article, we will see that this is very convenient. At any rate, here's an example:


    Example Program

    This is the same example as before, except that you should notice an exceptional increase in speed.
    -----------------] Example Starts here [-----------------
    
    PROGRAM Fonts;
    USES CRT,       { Need the Keypressed function }
         GraphPro;
    
    VAR
      Font : FontType;
      X, Y, DX, DY : INTEGER;
      Text : STRING;
    BEGIN
      InitGraph;
      ClrScr(1);
      LoadBIOSFont(Font, 8);
      WRITEG1(Font, 0, 0, 'This is size 8', 14);
      LoadBIOSFont(Font, 14);
      WRITEG1(Font, 0, 8, 'This is the BIOS''s 14 point font', 14);
    
      LoadBIOSFont(Font, 16);
      Font.Background := 14;
      WRITEG1(Font, 0, 22, 'This is a 16 point font inverted', 1);
    
      Font.Background := 0;
      WriteG1(Font, 0, 50, 'Press  to continue', 15);
      READLN;
    
      Font.Background := 13;
      ClrScr(Font.Background);
      X := 0; Y := 0; DX := 1; DY := 1;
      Text := ' Press any key to continue! ';
      WHILE Not Keypressed Do
      BEGIN
        X := X + DX;
        Y := Y + DY;
        IF (X = 0) OR (X = (319-8*LENGTH(Text))) THEN DX := -DX; { Bounce } 
        IF (Y = 0) OR (Y = (199-Font.Height)) THEN DY := -DY;    { Bounce }
        WriteG1(Font, X, Y, Text, 14);
      END;
      CloseGraph;
    END.
    
    -----------------] Example Ends here [-----------------


  • Created by Chris Lattner