
                             VGA-Kurs - Part #4

Wir kommen nun zu "T.C.P.'s Beginner's Guide to VGA Coding", Part IV.
Noch eine Anmerkung: Ich versuche immer, auch Lesern, die die ersten Teile des
Kurses verpat haben, die Mglichkeit zu geben, die Beispiele zu nutzen, auch
wenn ihnen die wichtigen Prozeduren fehlen. Der Nachteil ist, da ich z.B.
die schnelle PutPixel-Prozedur durch einen langsameren Speicherzugriff via
Mem ersetzen mu.
Nun eine Frage: Soll ich es so beibehalten oder in jedem Teil die bentigten
Prozeduren dazuschreiben?
Fr alle, die die ersten Teile des Kurses haben: Ihr solltet auf jeden Fall
folgende Prozeduren immer bereit halten:
PutPixel, WaitRetrace, ClrVGA. Ihr knnt diese Prozeduren dann in die Listings
einsetzen.
In diesem Teil werden wir Sprites behandeln, als Bonus gibt es einen kleinen
Abschnitt ber Code-Optimierung.
Auf dem PC sind Sprites immer noch fr viele ein Mythos, die vorher auf
Homecomputern wie Amiga/Atari/C64 gecodet haben, wo die Maschine alle
Sprite-Operationen erledigt.
Beim PC dagegen war am Anfang nie vorgesehen, da er einmal auch nur ein
Sprite ber den Bildschirm flitzen lassen wrde. Deshalb baute man solch eine
Funktion auch nie in die PC-Grafikkarten ein. Das Ergebnis ist, da wenn man
auf der VGA-Karte Sprites zaubern will, sich um alles selbst zu kmmern hat:
Darstellung, Bewegung, Kollisionsabfragen, Clipping, Durchscheinen des
Hintergrundes, runde Sprites etc.
Das dies erheblich auf die CPU geht, ist abzusehen, denn die VGA-Karte trgt
kein bischen dazu bei.
Allerdings kann man sich einiger Tricks behelfen, wie wir noch sehen werden.
Zu allererst sollte man sich einen Sprite-Editor zulegen. Davon gibt es
unzhlige als Shareware, man kann aber auch ein Malprogram wie DPaint
heranziehen.
Nun mu man die Sprites in ein Format bekommen, das man von Pascal aus leicht
lesen kann. Hierbei ist es gnstig, wenn man einen Sprite-Editor hat, der die
Sprites als RAW-Dump abspeichert. D.h., da die Pixel-Informationen
hintereinander in einer Datei abgelegt werden. Zeichnet man z.B. ein 32x32
Pixel groes Sprite, so hat man eine 1024 Byte groe Datei, die man direkt
einlesen kann:

type Sprite = array[0..1023] of byte;
var f : file;
    Spr : Sprite;
procedure Readsprite(str:string;s:Sprite);
begin
  assign(f,str);
  reset(f,1);
  blockread(f,s,1024);
  close(f);
end;

Diese Prozedur liest durch die Blockread-Prozedur 1024 Byte aus der Datei mit
dem Namen Str in die bergebene Variable des Typs Sprite.
Anmerkung: Wer den Autodesk Animator besitzt, kann die Sprites im CEL-Format
speichern. Dieses hat allerdings noch einen 800 Byte groen Header, der
per "seek(f,800)" bersprungen werden mu.
Es gibt aber auch Sprite-Editoren, die die Sprites gleich als Pascal-Source
speichern knnen (z.B. YC's Sprite Editor, den ich benutze). Dies hat dann
die folgende Form:

const Spr : Sprite = (
0,1,2,3,5,128,255,200,50,20,...
);

Nun, da wir hoffentlich unser Sprite in der Variablen Spr haben, wird es Zeit,
es auf den Bildschirm zu bringen:

procedure ShowSprite(x,y:word;s:Sprite);
var n1,n2 : byte;
begin
  for n1 := 0 to 31 do
    for n2 := 0 to 31 do mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2];
end;

Dies ist die einfachste Form der ShowSprite-Routine. Kein Clipping, kein
Durchscheinen, keine runden Sprites und das Wichtigste: Keine Geschwindigkeit!
Hier die Funktionsweise der Prozedur: In der Schleife wird ein 32x32 Pixel
groes Sprite an die Koordinaten (X,Y) gesetzt. Dazu wird die Adresse im
Bildschirmspeicher nach der bekannten Formel Adresse = 320 * Y + X berechnet
und dann die Position im Sprite-Array mittels Position = 32 * n1 + n2
dazugezhlt. Daraus ergibt sich die Formel Adresse = (n1+Y) * 320 + X + n2.
Schn und gut, aber was ist, wenn das Sprite rund ist, z.B. ein Fuball.
Wenn ich das Sprite auf einen schwarzen Hintergrund setze, macht es nichts,
aber wenn ich einen Hintergrund, wie z.B. ein Fuballfeld habe, und das Sprite
darauf setze, dann habe ich um den Ball einige schwarze Pixel. Das liegt
daran, da das Array, in dem das Sprite liegt, quadratisch ist, und wenn nun
im Sprite zwar kein Pixel vorgesehen ist, also im Array eine 0 steht, dann
wird trotzdem ein Pixel der Farbe 0 gesetzt.
Noch ein Problem: Hat man ein Sprite, da an gewissen Stellen durchsichtig
ist, z.B. eine Scheibe Schweizer Kse (Schei Beispiel, ich wei), dann sollte
an den Stellen, wo die Lcher sind, der Hintergrund durchscheinen, tut er aber
nicht, da wieder ein Pixel der Farbe 0 gesetzt wird.
Diese Probleme sind noch leicht zu lsen:

procedure ShowSprite(x,y:word;s:Sprite);
var n1,n2 : byte;
begin
  for n1 := 0 to 31 do
    for n2 := 0 to 31 do
      if s[n1*32+n2] <> 0 then mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2];
end;

Findet die Routine nun im Sprite ein Pixel der Farbe 0, so wird es erst gar
nicht gesetzt. Dadurch wird die Prozedur sogar schneller, da einige Pixel des
Sprites bersprungen werden, und so Zeit gespart wird.
Ein weiteres Problem ist, da wenn das Sprite am Rand des Bildschirms
angelangt ist und ber den Rand hinausgeht, es an der anderen Seite des
Bildschirms dargestellt wird. Das liegt am linearen Aufbau des
Bildschirmspeichers. Gehen die Spriteinformationen ber den Rand einer Zeile
hinaus, werden sie im Bildschirmspeicher weiter bewegt und erscheinen dadurch
in der nchsten Zeile. Dasselbe passiert, wenn das Sprite den unteren Rand des
Bildschirms berschreitet.
Ein Verfahren zum Vereiteln dieser Tatsache ist, da vor dem Setzen eines
Pixels berprft wird, ob er berhaupt noch in der Zeile bzw. Spalte liegt,
also X nicht grer als 319 und Y nicht grer als 199 ist. Dies nennt sich
Clipping.

procedure ShowSprite(x,y:word;s:Sprite);
var n1,n2 : byte;
begin
  for n1 := 0 to 31 do
    for n2 := 0 to 31 do
      if (s[n1*32+n2] <> 0) and (n2+x < 320) and (n1+y < 200) then
        mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2];
end;

Damit enthlt unsere Prozedur schon eine Menge Ifs und ist deshalb nicht
gerade als die schnellste zu bezeichnen.
Anders wre das schon bei der Assembler-Version, die zieht sich allerdings
etwas in die Lnge...

procedure ShowSprite(x,y,add:word);assembler;
asm
  mov     bx,31                        { Zhler mit Endwert initialisieren }
@loop1:                                { fr ein 32x32 Pixel Sprite }
  mov     cx,31
@loop2:
  mov     si,bx                        { n1 * 32 + n2 }
  shl     si,5                         { si * 32 }
  add     si,cx
  add     si,add                       { Variablen-Offset drauf }
  cmp     byte ptr ds:[si],0           { Pixelwert = 0 ? }
  je      @next                        { Wenn ja, nchster Pixel }
  mov     ax,cx
  add     ax,x
  cmp     ax,319                       { X-Koordinate > 319 ? }
  ja      @next                        { Wenn ja, nchster Pixel }
  mov     ax,bx
  add     ax,y
  cmp     ax,199                       { Y-Koordinate > 199 ? }
  ja      @next                        { Wenn ja, nchster Pixel }
  mov     ax,bx                        { (n1+y) * 320 + x + n2 }
  add     ax,y
  mov     dx,ax
  shl     ax,6                         { ax * 64 }
  shl     dx,8                         { dx * 256 }
  add     ax,dx
  add     ax,x
  add     ax,cx
  mov     di,ax
  mov     ax,0A000h                    { VGA-Segment nach ES }
  mov     es,ax
  mov     al,ds:[si]                   { Pixelwert aus Sprite-Daten holen }
  mov     es:[di],al                   { und auf VGA-Screen setzen }
@next:
  dec     cx                           { Zhler dekrementieren }
  jnz     @loop2                       { Wenn ungleich 0, innere Schleife }
  dec     bx                           { Zhler dekrementieren }
  jnz     @loop1                       { Wenn ungleich 0, uere Schleife }
end;

Diese Routine schafft immerhin schon rund 3000 Sprites pro Sekunde auf einem
DX/2 80, also ca. doppelt so viel wie die Pascal-Version.
Aber es geht (natrlich) noch schneller.
Die Vorgehensweise der Routine entnehmt ihr bitte den Kommentaren.

Folgende Tricks wurden in dieser Routine zur Optimierung verwendet:
1. Es wird nur das Offset des Sprites bergeben.
Statt das gesamte Sprite als Array an die Prozedur zu bergeben, wird ihr
nur das Offset des Sprites im Speicher mitgeteilt. Dadurch spart man es sich,
ganze 1022 Byte mehr zu bergeben.
Der Prozeduraufruf mu also nicht 'showsprite(x,y,Spritename)' lauten,
sondern 'showsprite(x,y,ofs(Spritename))'.
2. Es werden so viel Register wie mglich ausgenutzt.
Dies ist eine goldene Regel in der Assembler-Programmierung. Man sollte erst
auf Variablen zurckgreifen, wenn wirklich alle Register ausgenutzt sind.
3. Die Zhler werden dekrementiert.
Am Anfang werden die Zhler nicht mit dem Start- sondern mit dem Endwert
initialisiert. Danach werden sie in jeder Schleife dekrementiert. Dies hat
den Vorteil, das man sich ein langsames 'cmp zaehler,Endwert' erspart. Es
gengt der bedingte Sprungbefehl 'jnz', der automatisch erneut zum Anfang der
Schleife springt, wenn der Zhler ungleich 0 ist.
4. Statt 'mul' oder 'div' kommt 'shl' oder 'shr'.
Hat man eine Multiplikation bzw. Division mit bzw. durch einen Faktor der
ein vielfaches von 2 darstellt, kann man die Operation durch Bitverschiebung
beschleunigen. OK, wer hat den Satz verstanden? Niemand? Gut. Also:
Angenommen, man mu (wie in der Prozedur) eine Zahl mit 32 multiplizieren.
Dies geht vor allem auf Prozessoren unter 486 recht trge vonstatten.
Schneller geht es, wenn man die Zahl um 5 nach links shiftet, d.h. alle Bits
der Zahl um 5 Stellen nach links verschiebt. Denn: Eine Verschiebung um 1 Bit
erzeugt dasselbe Ergebnis wie eine Multiplikation mit 2. Ein Shiften um 2
Bits ist wie ein Malnehmen mit 4, 3 Bits wie mal 8, 4 Bits wie mal 16, usw.
Andersrum geht's auch. Ein Shiften um 1 Bit nach rechts ist wie eine Division
durch 2, wobei immer abgerundet wird.
Andere Tricks:
1. Exklusiv-Oder geschickt einsetzen.
Ein 'mov ax,0' ist identisch mit 'xor ax,ax', blo mit dem Unterschied, da
die zweite Art wesentlich schneller geht und als OpCode weniger Bytes
verbraucht.
2. So viel wie mglich raus aus der Schleife.
In Schleifen sollte nur das stehen, was dort auch wirklich hingehrt. Wenn
Schleifen unntige Befehle wie Variablenzuweisungen oder Rechenoperationen
enthalten, die auch auerhalb der Schleife ihren Dienst verrichten knnen,
sollte man sie rauswerfen.
3. Compiler-Optionen ausnutzen.
Folgende Compiler-Befehle am Anfang des Codes beschleunigen das Programm:

{$G+,D-,I-,Q-,R-,S-}

Damit wird alles, was bremst, abgeschaltet: Debug-Informationen,
In/Out-, Range- und Stack-Checking.

So, schn und gut, aber was ist, wenn wir unser Sprite ber einen Hintergrund
bewegen wollen? Also, wir setzen das Sprite auf unseren Hintergrund und
lschen es wieder, um es danach an eine andere Stelle zu setzen.
Wuups, da ist ja jetzt ein Loch in unserem schnen Hintergrund!
Tja, um dies zu lsen, knnte man sich Methode 1 oder 2 bedienen. Methode 1
ist, den Hintergrund, auf den das Sprite gesetzt wird, vor dem Setzen zu
sichern, und danach wieder herzustellen, wie in folgendem Schema:

var Buffer : Sprite;
    n1,n2  : byte;
begin
  setmcgamode;
  ErzeugeHintergrund;
  repeat
    for n1 := 0 to 31 do               { Ausschnitt sichern }
      for n2 := 0 to 31 do
        Buffer[n1*32+n2] := mem[$A000:(n1+y)*320+x+n2];
    showsprite(x,y,ofs(MySprite));     { Sprite zeichnen }
    showsprite(x,y,ofs(Buffer));       { HG wiederherstellen }
    BewegeSprite;                      { Neue Koordinaten }
  until Bedingung = true;
  settextmode;
end.

Gut, fr ein Sprite mag diese Methode ausreichen, aber was ist, wenn man z.B.
10 Sprites ber den HG bewegen will? Tja, jetzt mu Methode 2 herhalten.
Diese benutzt folgendes Schema:

begin
  setmcgamode;
  ErzeugeHintergrundAufVS;
  repeat
    KopiereHGAufVGA;
    ZeichneSprites;
  until Bedingung = true;
  settextmode;
end.

Moment maaal! Was soll den 'VS' sein? Nun, das ist der Virtual Screen. Also
ein 'virtueller Bildschirm'. Man kann ihn beschreiben und auslesen wie die
VGA. Aber das Wichtige ist: Man kann ihn auf die VGA kopieren, so, da sein
Inhalt auf dem Bildschirm erscheint.
Man erstellt also einen Virtual Screen (wie, werden wir noch sehen), und
erstellt einen Hintergrund auf ihm. Dann startet man die Schleife und kopiert
den Hintergrund aus dem VS auf die VGA. Nun zeichnet man seine Sprites darauf.
Aber wie erstelle ich nun so ein Teil???
Am einfachsten wre wohl die folgende Lsung:

var VS : array[0..63999] of byte;

Diese Variable knnte nun wie ein zweiter VGA-Bildschirm benutzt werden, aber:
Das Array ist 64000 Byte gro. Da man aber unter Pascal fr globale Variablen
nur maximal 65000 und ein paar Byte zur Verfgung hat, wrde es sehr eng
werden, und schnell kriegen wir die Compiler-Meldung 'Zuviele Variablen'.
Die Lsung fr unser Problem sind also: Zeiger (Pointer).
Die Deklaration des VS mu also so aussehen:

type BigArr = array[0..63999] of byte;
     VSPtr = ^BigArr;

var VS : VSPtr;
    VSAdd : word;

Nun mssen wir den VS nur noch initialisieren:

procedure InitVS;
begin
  getmem(VS,64000);
  VSAdd := seg(VS^);
end;

Nun haben wir fr den VS 64000 Byte an Speicher allokiert. Um ihn wieder
freizugeben sollte folgende Prozedur benutzt werden:

procedure CloseVS;
begin
  freemem(VS,64000);
end;

Will man nun auf den VS statt auf die VGA schreiben, ersetzt man die Adresse
des Bildschirmspeichers 'A000h:0' durch 'VSAdd:0'. Mit dem Mem-Befehl she das
so aus:

x := 50;
y := 80;
mem[VSAdd:y*320+x] := Col;

setzt auf dem VS an die Koordinaten (50,80) einen Pixel der Farbe Col.
Was aber ntzt mir das? Nun, nachdem wir den Hintergrund auf diese Weise auf
den VS gezeichnet haben, knnen wir ihn mit der folgenden Prozedur auf den
VGA-Screen kopieren:

procedure Flip;assembler;
asm
  push    ds                           { DS MU gesichert werden }
  mov     ax,0A000h                    { Zielsegment nach ES }
  mov     es,ax
  mov     ds,VSAdd                     { Quellsegment nach DS }
  xor     si,si                        { Offset = 0 }
  xor     di,di
  mov     cx,32000                     { 32000 Word (=64000 Byte) }
  rep     movsw                        { kopieren }
  pop     ds                           { DS wiederherstellen }
end;

Nun werden, wie besprochen, die Sprites darber gezeichnet, was natrlich
entsprechend flott vonstatten gehen sollte. Um Flickern und 'zerrissene'
Sprites zu vermeiden, sollte man auerdem noch die WaitRetrace-Prozedur
aufrufen.
Wer das mit dem Virtual Screen noch nicht ganz gepeilt hat (ist nicht einfach,
geb ich zu), der sei getrstet, ab der nchsten Ausgabe brauchen wir so was
nicht mehr, denn dann besprechen wir den VGA-Modus, der von Hause aus
4 Virtual Screens mitbringt, ohne auch nur ein Byte mehr Speicher zu
verbrauchen: Der Mode-X!
Also, ciao bis zum nchsten Teil.




[ This text copyright (c) 1995-96 Johannes Spohr. All rights reserved. ]
[ Distributed exclusively through PC-Heimwerker, Verlag Thomas Eberle. ]
[                                                                      ]
[ No  part   of  this   document  may  be   reproduced,   transmitted, ]
[ transcribed,  stored in a  retrieval system,  or translated into any ]
[ human or computer language, in any form or by any means; electronic, ]
[ mechanical,  magnetic,  optical,   chemical,  manual  or  otherwise, ]
[ without the expressed written permission of the author.              ]
[                                                                      ]
[ The information  contained in this text  is believed  to be correct. ]
[ The text is subject to change  without notice and does not represent ]
[ a commitment on the part of the author.                              ]
[ The author does not make a  warranty of any kind with regard to this ]
[ material, including,  but not limited to,  the implied warranties of ]
[ merchantability  and fitness  for a particular  purpose.  The author ]
[ shall not be liable for errors contained herein or for incidental or ]
[ consequential damages in connection with the furnishing, performance ]
[ or use of this material.                                             ]
