There are many different kinds of sprites, but the kind we're going to tackle
here are just a rectangular array, one byte for every pixel. The size of the
array is simply XSize*YSize, where X and YSize are simply the size of the sprite.
The need to get and to set sprites is in the heart of animation. Lets say that
you're programming a pinball game. The ball would be a sprite, along with the
flippers and the obstacles on the table. This table would be "painted" with
a theme corresponds to the whole machines theme.
When the ball moves, you would calculate the new position, and draw the sprite
there. The next frame you do the same. But what happens to the cool table
underneath the sprite? It is obscured and destroyed. To fix this, we change
the loop to this:
Here is the Pascal only version. I've already converted it to a incrementally
index version...
A sprite is an array of data that holds a rectangular picture. This picture can
be sent to the screen, rapidly, to display a bitmap. Common uses for it are in
a menuing system, before the menu is drawn on the screen, a copy of the screen
underneath the menu is saved, so that it can be restored without redrawing the
entire screen.
For this to work, we need facilities to both set AND get sprites... Fast ones
if we have anything to say about it!
To get a sprite, we need to capture an area of the screen and store it in
memory as the sprite. For this task, we'll use the mighty GetPixel!! Here
it is:
PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
X, Y : INTEGER;
BEGIN
CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
FOR Y := Y1 TO Y2 DO
FOR X := X1 TO X2 DO
Sprite.Data^[Y*Sprite.Width+X] := GetPixel(X, Y);
END;
This is a reasonably fast Get routine, but it could be made faster by not
calculating the index into the sprite data array each time. Since we access
sequential index elements, we can simply increment it as we go...
PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
Index, X, Y : INTEGER;
BEGIN
CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
Index := 0;
FOR Y := Y1 TO Y2 DO
FOR X := X1 TO X2 DO
BEGIN
Sprite.Data^[Index] := GetPixel(X, Y);
INC(Index);
END;
END;
This is as fast as we can go in plain Pascal... It's time to switch gears into
assembler!
The first step to converting this is to change the inner loop... Therefore:
PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
SprIndex, X, Y : INTEGER;
ScrIndex : WORD;
BEGIN
CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
SprIndex := 0;
FOR Y := Y1 TO Y2 DO
BEGIN
ScrIndex := Y*Screen.Width+X1;
FOR X := X1 TO X2 DO
ASM
push DS
les DI, Sprite { VAR parameters are actually passed as }
les DI, ES:[DI+SpriteType.Data] { pointers to the variable... }
add DI, SprIndex
lds SI, Screen.Buffer
add SI, ScrIndex
movsb { Sprite.Data^[SprIndex] := GetPixel(X, Y) }
pop DS
inc ScrIndex
inc SprIndex { INC(SprIndex) }
END;
END;
END;
Obviously, we can eliminate a number of things from this loop if we include
the FOR X loop in the assembler block. Many segment register loads and memory
references can be removed... Here we go!
PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
SprIndex, Y : INTEGER;
ScrIndex : WORD;
BEGIN
CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
SprIndex := 0;
ScrIndex := Y1*Screen.Width+X1;
FOR Y := Y1 TO Y2 DO
ASM
mov CX, X2
sub CX, X1
inc CX { Calculate DeltaX -> X2-X1+1 }
push DS
les DI, Sprite { VAR parameters are actually passed as }
les DI, ES:[DI+SpriteType.Data] { pointers to the variable... }
add DI, SprIndex
lds SI, Screen.Buffer
add SI, ScrIndex
rep movsb
mov SprIndex, DI { Auto-incremented by the movsb instruction }
pop DS
mov AX, Screen.Width
add ScrIndex, AX
END;
END;
That very inner loop can easily converted to a rep movsb instruction... Cool.
Now we move in one level and optimize the YLoop...
PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER); ASSEMBLER;
VAR ScrIndex : WORD;
ASM
mov DX, X2
sub DX, X1 { Calculate DeltaX -> X2-X1+1 }
inc DX
mov BX, Y2
sub BX, Y1 { Calculate DeltaY -> Y2-Y1+1 }
inc BX
push DX { Save a copy for later. }
push BX
db $66; mov AX, WORD [Sprite]
db $66; push AX
push DX
push BX
call CreateSprite { CreateSprite(Sprite, X2-X1+1, Y2-Y1+1) }
mov AX, X1
mov BX, Y1 { Y1 is an index into the ScreenY array }
add BX, BX { Multipy by two because each entry is a word }
add AX, DS:[BX+OFFSET Screen.YTable]
mov ScrIndex, AX { ScrIndex := Y1*Screen.Width+X1 }
pop BX { Restore YDelta }
pop DX { Restore XDelta }
mov AX, Screen.Width { Can't get this with DS mangled... }
sub AX, DX { Calculate Screen.Width-Sprite.Width }
push DS
les DI, Sprite { VAR parameters are actually passed as }
les DI, ES:[DI+SpriteType.Data] { pointers to the variable... }
lds SI, Screen.Buffer
add ScrIndex, SI
@YLoop: { FOR Y := Y1 TO Y2 DO }
mov CX, DX { Copy the correct number of pixels! }
rep movsb { Do one scan line. }
add SI, AX { Increment the ScrIndex to the next line }
dec BX
jnz @YLoop { Next YLoop... }
pop DS
END;
That's it. Notice that there is a complete lack of multiply instructions...
This routine is fast, although the call to CreateSprite makes it not as fast
as DrawSprite...
Okay, now we can transfer a block of data from the screen to memory... What
good does that do us? None, unless we have a routine that can transfer it
back! This DrawSprite routine needs to be fast and furious, and it has to
be easy to use.
PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
Index, XI, YI : INTEGER;
BEGIN
Index := 0;
FOR YI := 0 TO Sprite.Height-1 DO
FOR XI := 0 TO Sprite.Width-1 DO
BEGIN
SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
INC(Index);
END;
END;
As you can see, the only differance between GetSprite and DrawSprite is the
one line that calls SetPixel. To optimize the inner loop, we would to the
following:
PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
ScrIndex : WORD;
SprIndex, XI, YI : INTEGER;
BEGIN
SprIndex := 0;
FOR YI := 0 TO Sprite.Height-1 DO
BEGIN
ScrIndex := (Y+YI)*Screen.Width+X;
FOR XI := 0 TO Sprite.Width-1 DO
ASM
push DS
les DI, Screen.Buffer
add DI, ScrIndex
lds SI, Sprite
lds SI, DS:[SI+SpriteType.Data]
add SI, SprIndex
movsb { SetPixel(XI+X, YI+Y, Sprite.Data^[SprIndex] }
pop DS
inc ScrIndex
inc SprIndex { INC(SprIndex) }
END;
END;
END;
Does this look remarkably familiar? It's very like the first optimization of
GetSprite! Hmmm... Here's the next loop optimized...
PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
ScrIndex : WORD;
SprIndex, YI : INTEGER;
BEGIN
SprIndex := 0;
ScrIndex := Y*Screen.Width+X;
FOR YI := 0 TO Sprite.Height-1 DO
ASM
push DS
les DI, Screen.Buffer
add DI, ScrIndex
lds SI, Sprite
mov CX, DS:[SI+SpriteType.Width]
lds SI, DS:[SI+SpriteType.Data]
add SI, SprIndex
@XLoop:
movsb
dec CX
jnz @XLoop
pop DS
mov SprIndex, SI { INC(SprIndex) }
mov AX, Screen.Width
add ScrIndex, AX
END;
END;
Now when we convert it all to assembler, we see the true power of assembler.
Doing it yourself. When Turbo Pascal compiled to original code, it required
a pointer load for every pixel. It also required that there was a call to
SetPixel for every pixel... This was sloooowww... To see the improvement,
follow the suggestion in the example.
PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER); ASSEMBLER;
ASM
mov AX, X
mov BX, Y { Y1 is an index into the ScreenY array }
add BX, BX { Multipy by two because each entry is a word }
add AX, DS:[BX+OFFSET Screen.YTable]
mov BX, AX { ScrIndex := Y*Screen.Width+X }
mov AX, Screen.Width { Save this for later when we can't get to DS }
push DS
les DI, Screen.Buffer
add DI, BX { Add DI, ScrIndex... }
lds SI, Sprite
mov DX, DS:[SI+SpriteType.Width] { Grab XDelta }
mov BX, DS:[SI+SpriteType.Height] { Grab YDelta }
sub AX, DX { Calculate Screen.Width-Sprite.Width }
lds SI, DS:[SI+SpriteType.Data]
@YLoop: { FOR YI := 0 TO Sprite.Height-1 DO }
mov CX, DX { Copy the correct number of pixels! }
rep movsb { Do one scan line. }
add DI, AX { Inc ScrIndex to the next scanline }
dec BX
jnz @YLoop { Next YLoop... }
pop DS
END;
Both of these examples are very efficient, and they are compact. They also
use six of the eight general purpose registers... They don't touch BP and SP.
These routines are the basis for the sprite series, so it's a good thing they
go fast! :)
This example program demostrates the basics of handling sprites... If you want
to see the differance that optimization makes, just set SlowSprite to true.
It then uses the Pascal version of the sprite routines instead of the assembler
one... It's a huge differance!
{{$DEFINE SlowSprite } { Just remove a curly bracket to enable... }
{{$DEFINE WaitRetrace } { Remove this to see it nice and slow... }
USES
CRT, { KeyPressed routine }
GraphPro; { GetPixel and SetPixel }
TYPE
ByteArrayType = ARRAY[0..65534] OF BYTE;
ByteArrayPtr = ^ByteArrayType;
SpriteType = RECORD { Each record is convieniently 8 bytes long }
Height, Width : INTEGER; { Height and Width of sprite }
Data : ByteArrayPtr;{ Pointer to the color data }
END;
PROCEDURE KillSprite(VAR Sprite : SpriteType);
BEGIN
WITH Sprite DO
BEGIN
FREEMEM(Data, Width*Height);
Width := 0; Height := 0; Data := NIL;
END;
END;
PROCEDURE CreateSprite(VAR Sprite : SpriteType; Width, Height : INTEGER);
BEGIN
IF Sprite.Data <> NIL THEN KillSprite(Sprite);
Sprite.Width := Width;
Sprite.Height := Height;
GETMEM(Sprite.Data, Width*Height);
END;
{$IFDEF SlowSprite} { Use the Pascal version.... }
PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER);
VAR
Index, X, Y : INTEGER;
BEGIN
CreateSprite(Sprite, X2-X1+1, Y2-Y1+1);
Index := 0;
FOR Y := Y1 TO Y2 DO
FOR X := X1 TO X2 DO
BEGIN
Sprite.Data^[Index] := GetPixel(X, Y);
INC(Index);
END;
END;
PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
Index, XI, YI : INTEGER;
BEGIN
Index := 0;
FOR YI := 0 TO Sprite.Height-1 DO
FOR XI := 0 TO Sprite.Width-1 DO
BEGIN
SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
INC(Index);
END;
END;
{$ELSE} { Use the Assembler version... }
PROCEDURE GetSprite(VAR Sprite : SpriteType; X1, Y1, X2, Y2 : INTEGER); ASSEMBLER;
VAR ScrIndex : INTEGER;
ASM
mov DX, X2
sub DX, X1 { Calculate DeltaX -> X2-X1+1 }
inc DX
mov BX, Y2
sub BX, Y1 { Calculate DeltaY -> Y2-Y1+1 }
inc BX
push BX { Save a copy for later. }
push DX
db $66; mov AX, WORD [Sprite]
db $66; push AX
push DX
push BX
call CreateSprite { CreateSprite(Sprite, X2-X1+1, Y2-Y1+1) }
mov AX, X1
mov BX, Y1 { Y1 is an index into the ScreenY array }
add BX, BX { Multipy by two because each entry is a word }
add AX, DS:[BX+OFFSET Screen.YTable]
mov ScrIndex, AX { ScrIndex := Y1*Screen.Width+X1 }
pop DX { Restore XDelta }
pop BX { Restore YDelta }
mov AX, Screen.Width { Can't get this with DS mangled... }
push DS
les DI, Sprite { VAR parameters are actually passed as }
les DI, ES:[DI+SpriteType.Data] { pointers to the variable... }
lds SI, Screen.Buffer
add SI, ScrIndex
sub AX, DX { Calculate Screen.Width-Sprite.Width }
@YLoop: { FOR Y := Y1 TO Y2 DO }
mov CX, DX { Copy the correct number of pixels! }
rep movsb { Do one scan line. }
add SI, AX { Increment the ScrIndex to the next line }
dec BX
jnz @YLoop { Next YLoop... }
pop DS
END;
PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER); ASSEMBLER;
ASM
mov AX, X
mov BX, Y { Y1 is an index into the ScreenY array }
add BX, BX { Multipy by two because each entry is a word }
add AX, DS:[BX+OFFSET Screen.YTable]
mov BX, AX { ScrIndex := Y*Screen.Width+X }
mov AX, Screen.Width { Save this for later when we can't get to DS }
push DS
les DI, Screen.Buffer
add DI, BX { Add DI, ScrIndex... }
lds SI, Sprite
mov DX, DS:[SI+SpriteType.Width] { Grab XDelta }
mov BX, DS:[SI+SpriteType.Height] { Grab YDelta }
sub AX, DX { Calculate Screen.Width-Sprite.Width }
lds SI, DS:[SI+SpriteType.Data]
@YLoop: { FOR YI := 0 TO Sprite.Height-1 DO }
mov CX, DX { Copy the correct number of pixels! }
rep movsb { Do one scan line. }
add DI, AX { Inc ScrIndex to the next scanline }
dec BX
jnz @YLoop { Next YLoop... }
pop DS
END;
{$ENDIF}
VAR
BackgroundSprite,
TestSprite : SpriteType;
XD, YD, { X and Y Directions }
OX, OY, { Old X and Y's }
I, X, Y : INTEGER;
BEGIN
WRITE('This is a test of the sprite system... The current mode is: ');
{$IFDEF SlowSprite} WRITELN('Pascal Routines');
{$ELSE } WRITELN('Assembler Routines'); {$ENDIF}
WRITELN;
WRITELN('The sections are in the following order: ');
WRITELN;
WRITELN('1. Large Sprite moving across the screen.');
WRITELN('2. Sprites that don''t clean up after themselves.');
WRITELN('3. Sprites that DO clean up....');
WRITELN;
WRITELN('Press