home *** CD-ROM | disk | FTP | other *** search
- MCGA Graphics Tutorial
- Lesson #5
- by Jim Cook
-
- Welcome to the fifth installment of the MCGA tutorial. Those of you
- that have followed this tutorial since its inception realize that I have
- really slacked off lately. I will try to make it up to you. Of course,
- since I can't give you all cash, I am offering code. I wish to
- start getting into some animation techniques.
-
- The basis for any animation is simply:
-
- Save Background -> Put Image -> Repeat
-
- In MCGA #4 we learned about the PCX format and how to put an entire PCX
- or even a smaller PCC on the screen. Next we will learn how to cut out
- a section of the screen (usually the background) and then redisplay it.
- Those of you familiar with Borland's BGI are saying, "Sure, GetImage and
- PutImage!" You are exactly correct, except Borland's BGI lacks one
- thing...SPEEED!!! It is not directly their fault, since they did not
- write GRAPH.TPU with SPEEED in mind, but rather flexibility. What we
- will do is implement our own version of GetImage and PutImage.
-
- The first thing we need to do, is allocate some memory to put this piece
- of the screen we will be cutting out. Our experience with PCX files
- reminds us that before we allocate memory we need to determine how much
- memory we need. This is a very simple technique because each pixel in
- MCGA is equivalent to one byte of video memory (see MCGA #2), hence:
-
- Function ImageSize (X1,Y1,X2,Y2:Integer) : Word;
- begin
- ImageSize := Word(Y2 - Y1 + 1) * Word(X2 - X1 + 1) + 4;
- end;
-
- You might be asking yourself, "Why add four?". Well, along with the
- picture data (num pixels high * num pixels wide), we are going to store
- the width and height of the saved image. You will notice in the
- declarations of PutImage we only specify the upperleft corner of where
- we want to place the image. This is because the height and width are
- stored along with the image data.
-
- Procedure GetImagePas (X1,Y1,X2,Y2:Integer;P:Pointer);
- Procedure PutImagePas (X1,Y1:Integer;P:Pointer);
-
- The routines themselves aren't terribly difficult.
-
- type
- PointerType = array [0..65500] of byte;
- NewPointer = ^PointerType;
-
- Procedure GetImagePas (X1,Y1,X2,Y2:Integer;P:Pointer);
- var
- I : Integer;
- Count,
- ScnPos,
- Wide,High : Word;
- 8: Buf : NewPointer absolute P;
- begin
- 1: Wide := (X2 - X1) + 1;
- High := (Y2 - Y1) + 1;
- 2: ScnPos := (Y1 * 320) + X1;
- Count := 4;
-
- 3: Move (Wide,Buf^[0],SizeOf(Wide));
- Move (High,Buf^[2],SizeOf(High));
-
- 4: For I := 1 to High do begin
- 5: Move (Mem[$A000:ScnPos],Buf^[Count],Wide);
- 6: Inc (ScnPos,ScreenWide);
- 7: Inc ( Count,Wide);
- end;
- end;
-
- 1: Calculate the width and height of the image.
- 2: Calculate the screen offset in video memory where the image begins
- 3: Store the width and height of the image as the first two words
- in the buffer. Note how this is done. You may be wondering why I'm
- referring to Buf, when the pointer that was passed is called P. Take
- a look at 8:. There, I absolute a different data type on P. You can
- think of "absolute" like laying a new type of data on top of an
- existing one. In Pascal, you can't refer to a specific offset
- pointed to by a pointer like you can in C. I could of said:
- Move (High,Ptr(Seg(Buf^),Ofs(Buf^)+2)^,SizeOf(High));
- But I choose the former method for obvious clarity.
- 4: Next we set up a loop for each row
- 5: Here is the meat of the routine. The first time through the loop,
- Mem[$A000:ScnPos] refers to the address of the first pixel in the
- section we are cutting out. Actually, copying out is a better term.
- Count keeps track of where in the storage buffer we are currently
- positioned. So we are moving an entire row (WIDE) of pixels from
- video memory and into our storage buffer.
- 6: Next we point our screen position to the beginning of the next row.
- 7: And increment our storage buffer pointer to the next free position.
-
- I hope that explains it well enough. If there are parts you don't
- understand. it would probably do you some good to reread MCGA #2.
-
- I detest what I am about to write and I'll tell you why. Have you ever
- repaired your car or truck with a socket wrench in one hand and a
- Chilton's manual in the other? After you have taken the cylinder head
- off your car and replace some intricate part they always state,
- "To replace the part, follow the reverse of the original procedure."
- I hate that! To round out my hypocrisy, I am about to do the same to
- you. At least you can post a message if it is not clear:
-
- Procedure PutImagePas (X1,Y1:Integer;P:Pointer);
- var
- I : Integer;
- Count,
- ScnPos,
- Wide,High : Word;
- Buf : NewPointer absolute P;
- begin
- ScnPos := (Word(Y1) * 320) + Word(X1);
- Count := 4;
-
- Move (Buf^[0],Wide,SizeOf(Wide));
- Move (Buf^[2],High,SizeOf(High));
-
- For I := Y1 to Y1+High-1 do begin
- Move (Buf^[Count],Mem[$A000:ScnPos],Wide);
- Inc (ScnPos,ScreenWide);
- Inc ( Count,Wide);
- end;
- end;
-
- Okay, now add these routines to your MCGALIB unit. Following is a
- program to test the effectiveness of the procedures. I have benchmarked
- (and I use the term loosely) the copying of a 60 by 40 chunk of screen at
- 325 displays per second. I have also achieved 15 full screens per
- second with these routines:
-
- Program MCGATest;
-
- uses
- Crt,Dos,MCGALib;
-
- var
- Stop,
- Start : LongInt;
- Regs : Registers;
- PicBuf,
- StorageBuf : Pointer;
- const
- NumTimes = 1000;
-
- Procedure LoadBuffer (S:String;Buf:Pointer);
- var
- F : File;
- BlocksRead : Word;
- begin
- Assign (F,S);
- Reset (F,1);
- BlockRead (F,Buf^,65000,BlocksRead);
- Close (F);
- end;
-
- Function Tick : LongInt;
- begin
- Regs.ah := 0;
- Intr ($1A,regs);
- Tick := Regs.cx shl 16 + Regs.dx;
- end;
-
- Procedure ShowAndTell;
- var
- Ch : Char;
- NumSecs,
- NumTicks,
- SecsPerIter,
- TicksPerSec,
- TicksPerIter : Real;
- begin
- TextMode (3);
- NumTicks := Stop - Start;
- NumSecs := NumTicks / 18.2;
- TicksPerIter := NumTicks / NumTimes;
- SecsPerIter := NumSecs / NumTimes;
- TicksPerSec := 18.2 / TicksPerIter;
-
- Write ('After ',NumTimes,' iterations ');
- WriteLn ('and ',NumSecs:6:4,' seconds...');
- Write (' Each iteration took ',TicksPerIter:6:4,' ticks or ');
- WriteLn (SecsPerIter:4:3,' seconds!');
- WriteLn (' That''s about ',TicksPerSec:6:4,' times per second.');
- Repeat Until Keypressed;
- While Keypressed do Ch := Readkey;
- end;
-
- Procedure Control;
- var
- I,X,Y : Integer;
- Size : Word;
- begin
- SetGraphMode ($13);
- LoadBuffer ('E:\NAVAJO.PCX',PicBuf);
-
- DisplayPCX (0,0,PicBuf);
-
- Size := ImageSize (40,60,100,100);
- GetMem (StorageBuf,Size);
-
- GetImagePas (40,60,100,100,StorageBuf);
-
- ClearScreen (0);
-
- Start := Tick;
- For I := 1 to NumTimes do begin
- X := Random (280);
- Y := Random (140);
- PutImagePas (X,Y,StorageBuf);
- end;
- Stop := Tick;
- ShowAndTell;
- end;
-
- Procedure Init;
- begin
- GetMem (PicBuf,65500);
- end;
-
- Begin
- Init;
- Control;
- End.
-
- I would like to take a minute to pose a puzzlement. The pascal Get and
- Put routines make extensive use of the Move command. The Move command
- copies data from the Source pointer to the Destination pointer one byte
- at a time. In the interest of SPEEED, I turned on 286 code in my
- compiler options box, expecting move to start moving data a word at
- time, but found no increase in SPEEED. I guess since this is not an
- optimizing compiler, the MOVSW command is not substituted for even
- amounts of MOVSB's. So.......the next best thing is writing our own:
-
- Procedure Move (Var Source,Dest;Count:Word);
- begin
- asm
- push ds
- lds si,Source
- les di,Dest
- mov cx,Count
- shr cx,1
- rep movsw
-
- mov cx,Count
- test cl,1
- jz @@EvenCount
- movsb
- @@EvenCount:
- pop ds
- end;
- end;
-
- After adding this procedure at the beginning of the MCGALIB unit, my
- speeeds were increased 20%! I would have to say that this is as close
- to top speeed that Pascal gets. Soooo, as usual, we move our procedures
- into the realm of assembly language and enjoy even faster processing.
- Take a look:
-
- Procedure GetImageAsm (X1,Y1,X2,Y2:Integer;P:Pointer); assembler;
- asm
- mov bx,ScreenWide
- push ds
- les di,P
-
- mov ax,0A000h
- mov ds,ax
- mov ax,Y1
- mov dx,320
- mul dx
- add ax,X1
- mov si,ax
-
- mov ax,X2
- sub ax,X1
- inc ax
- mov dx,ax
- stosw
-
- mov ax,Y2
- sub ax,Y1
- inc ax
- stosw
- mov cx,ax
-
- @@1:
- mov cx,dx
-
- shr cx,1
- rep movsw
-
- test dx,1
- jz @@2
- movsb
- @@2:
- add si,bx
- sub si,dx
-
- dec ax
- jnz @@1
-
- pop ds
- end;
-
- Procedure PutImageAsm (X1,Y1:Integer;P:Pointer); assembler;
- asm
- mov bx,ScreenWide
- push ds
- lds si,P
-
- mov ax,0A000h
- mov es,ax
- mov ax,Y1
- mov dx,320
- mul dx
- add ax,X1
- mov di,ax
-
- lodsw
- mov dx,ax
-
- lodsw
-
- @@1:
- mov cx,dx
-
- shr cx,1
- rep movsw
-
- test dx,1
- jz @@2
- movsb
- @@2:
- add di,bx
- sub di,dx
-
- dec ax
- jnz @@1
-
- pop ds
- end;
-
- Following are the times I realized on a 386 33mhz Packard Bell. The
- measurements are in iterations per second. I broke the times to show
- the slight difference between Pascal and assembly code and the larger
- difference between word moves and byte moves:
-
- Image Size Pascal Assembler
-
- Byte Word Byte Word
-
- 100 x 100 89 115 102 147
-
- 320 x 200 15 29 16 32
- (full screen)
-
- Also note, that the full screen word moves approach the speeds necessary
- for full screen motion, which is 30 frames per second. In the assembly
- source we actually exceed that standard.
-
- So now that we are blazing fast....what next? Stay tuned for MCGA #7.
- We are close to building an animation model. I think the next thing we
- need to cover is page flipping and since this video mode only supports
- one video page, we will have to come up with another method. You can
- bet it will have something to do with word moves!
-
- MCGA #6 is a listing showing the MCGALIB we have created thus far.
-
- jim
-
-
-
-
-
-
-
-
-
-