All of this leads up to thing... the actual routine.
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!
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!
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:
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:
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:
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:
This is the same example as before, except that you should notice an exceptional increase in speed.
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.