Programmierseite - Interessante Themen
<< Zurück Alle Angaben ohne Gewähr. Benutzung auf eigenes Risiko. (C) Copyright Bernd Noetscher 1995 - 2000 eMail: webmaster@berndnoetscher.de |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Assembler This software may be used and distributed according to the terms of the GNU Public
License, . |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1. Grundlagen: Zusammenspiel Prozessor und Programm Bevor man so richtig loslegen kann, muß natürlich erst einmal ein gewisses Basiswissen vorhanden sein. Deshalb muß vor der eigentlichen Grafikprogrammierung Grundlegendes erläutert werden. So wird in den späteren Kapiteln alles jetzt Erwähnte vorausgesetzt. Für die Hardware- bzw. Assemblerprogrammierung ist die Kenntnis der Datentypen mit ihren Wertebereiche und deren Platzbedarf sehr wichtig. Die einzelnen Datentypen unterscheiden sich letztlich lediglich durch ihren Speicherplatzbedarf im RAM und durch das verschiedene Ansprechen durch die CPU. Das heißt, alle Datentypen bestehen aus Bytekettten unterschiedlicher Größe und werden erst durch eine unterschiedliche Sichtweise (denn Bytes sind Bytes) unterscheidbar. Z. B. kann die Bytefolge "01 65" als 16bit Zahl angesehen werden oder als Zeichenkettenvariable mit einer Länge von 1 und dem Großbuchstaben A (entspricht der Pascal-Konvention; führendes Byte enthält die Stringlänge).
Es gibt natürlich noch viel mehr Datentypen (Gleitkommavariablen, Zeichenketten usw.), wichtig sind aber zur Grafikprogrammierung (gemeint ist die Darstellung, keine 3D-Berechnungen) im eigentlichen Sinne nur die aufgeführten. Die Datentypen müssen entsprechend der Registerbreite bzw. der Ergebnisgröße der math. Funktionen gewählt werden. Dabei sollte man so wenig wie möglich Speicherplatz benötigen, damit von vornherein Platzverschwendung vermieden wird, denn es rächt sich später, wenn eine zeitraubende Umdefinierung, aufgrund von Speichermangel, notwendig wird. Z. B. braucht das Ergebnis einer Multiplikation von zwei Variablen vom Typ Byte/ShortInt (1 Byte * 1 Byte) mind. einen Speicherplatz von Typ Integer/Word (2 Byte), denn das Ergebnis könnte größer als der Wertebereich einer Byte-/ShortIntvariable sein. Das Ergebnis muß in der Regel so groß wie die Summe aus den Speichergrößen der Operanden sein. Bei Datentypen mit Vorzeichen muß ein Bit das Vorzeichen speichern. Dadurch vermindert sich natürlich die Bitanzahl zur Darstellung der Zahl um eins. Es wird der Wertebereich um die Hälfte "reduziert". Der Prozessor selbst kann signed und unsigned nicht unterscheiden. Die Unterscheidung wird es durch entsprechende Befehle möglich. Der Programmierer muß durch die entsprechenden Befehle dem Prozessor mitteilen, ob es sich um signed oder unsigned Variablen handelt. Häufig müssen die einzelnen Bits von Variablentypen mit mehreren Bytes getrennt angesprochen werden. Z. B. besteht eine Wordvariable aus zwei Bytes von dem das höherwertige Hibyte (rechtes Bytestück) und das niederwertige Lobyte (linkes Bytestück) genannt wird. Anordnung einzelner Bits der Variablen Intgeger/Word und Long Integer:
Übriges besteht ein Unterschied in der Speicherreihenfolge der Byteteile einer Variablen zwischen den Intelsystemen und den Macsystemen. Bei Intel wird zuerst Hi, und dann Lobyte abgelegt, was bei Macintosch genau umgekehrt geschieht. Dies ist insofern interessant, wenn ein Datenaustausch zwischen beiden Systemen vorgenommen werden muß, dann muß nämlich die Reihenfolge der Lo- und Hibytes beachtet werden.
Wer eine Hochsprache beherrscht und dort jahrelang Programme entwickelt hat, wird auf den ersten Blick überrascht sein, denn es sind nur einfache Befehle mit nur maximal zwei Operanden möglich.
Nun stellt sich die Frage, was ist ein Register. Technisch gesehen ist ein Register ein sehr schneller Speicher (sehr viel schneller als der Arbeitsspeicher), der nur eine kleine Kapazität besitzt. Genauer gesagt, kann das größte Register 4 Bytes speichern. Doch keine Angst, die wenigen Bytes reichen vollkommen aus, um auch die komplexesten Probleme in der EDV zu lösen. Die Register sind historisch in ihrer Größe gewachsen, so waren die ersten Register lediglich 1 Byte groß. Später gebrauchte man 2 byte große Register, die MS DOS und die älteren Programme benutzen. Heute sind die sogenannten 32 bit großen Register (z. B. 32-bit-Programmierung von OS/2 oder Windows 95), also 4 byte große Register bekannt. Die großen 4-byte-Register bieten den Vorteil, daß sie ohne großen Aufwand auch die sehr große Long Integer-Zahl in nur einem Register verwalten können (was bei 16 bit zwei Register erfordert). Zusätzlich wurde das leidige Thema der Segmentierung des Arbeitsspeichers "abgeschafft". Darin liegt der Hauptvorteil, denn nun kann man auch in einfacher Weise größere Arbeitsspeicherabschnitte (>64 KB) verwalten (betrifft Kopieren, Verschieben, Beschreiben und Lesen, ohne auch nur auf irgendwelche Segmentbegrenzungen achten zu müssen). Aus Sicht des Programmierers entspricht ein Register im Grunde einer Variablen vom Typ Word oder Integer. Denn manche dieser Register ermöglichen es, mathematische Berechnungen durchzuführen, andere jedoch dienen wiederum einfach nur zum reibungslosen Ablauf des Maschinenprogrammes. Die Register werden natürlich durch ihre Funktionsweise unterschieden.
Die allgemeinen Register werden für die eigentliche Verarbeitung der Daten genutzt. Mit ihnen werden die mathematischen Berechnungen (aber keine Kommazahlen), (Sprung-)Vergleiche, Schleifensteuerungen und Schreiben/Lesen des Arbeitsspeichers (vor allem in Einzelschritten) möglich. Die Namen dieser Register lauten AX, BX, CX und DX (bei der 32bit-Version wird einfach ein E für Extended vorangestellt, z. B. EAX). Wobei die Register AX und DX am häufigsten gebraucht werden. All diese vier Register sind gleich groß und gleichartig aufgebaut. Das Register EAX ist ein 32 bit Register, dessen Loword AX als 16bit-Teil angesprochen werden kann. Dieser 16bit-Teil wird nochmals in zwei 8bit-Teile in Lo- und Hibyte (AL, AH) unterteilt.
Mit diesen Registern kann im Grunde alles gemacht werden (Vergleiche, Berechnungen usw.), ohne auf irgendwelche Vorsichtsmaßnahmen oder mögliche Computerabstürze zu achten. Jedoch wird das Register ECX als Zähler bei vielen Befehlen verwendet und sollte deshalb auch nur für diesen Zweck verwendet werden. Die folgende Tabelle zeigt uns, daß nur die Datentypen und die Register zusammen passen, die auch die gleiche Bytegröße besitzen.
Nach der harten Theorie nun die Praxis. Anders als in den Hochsprachen muß in Assembler jede Aufgabe Schritt für Schritt zerlegt werden. So muß auch der Compiler von Pascal bei der Umwandlung von Hochsprache in Maschinensprache alle Befehle, mathematischen Berechnungen usw. analysieren und die verschiedenen Operatoren in die entsprechende Maschinenbefehle übersetzen. Leider ist in Borland Pascal 7.0 das Ansprechen der 32-bit-Register (EAX, EBX, ECX, EDX, ESI usw.) nicht möglich. Der dazugehörige Protected Mode Handler (RTM.EXE, DPMI16BI.OVL) arbeitet im 16bit-Modus. Kurz und klar bedeutet dies, bei der späteren Grafikprogrammierung Schwierigkeiten mit Speicherbereichen, größer 64 KB. Bleibt zu hoffen, daß Sie ein glücklicher Besitzer eines 32-bit-Compilers sind, bei dem der 32-bit-Protected-Mode genutzt wird (z. B. Watcom V10.6 C/C++), dann können Sie einfach und ohne Probleme auch die größten Speicheranforderungen bewältigen. Die mathematischen Ausdrücke müssen in ihre einzelnen Bestandteile zerlegt werden und so Operation für Operation nacheinander abgearbeitet werden. So müssen bei dem Ausdruck
Zu beachten sind bei Multiplikation/Division die Unterscheidung zwischen vorzeichenlosen/-behafteten Variablen/Speicherplätzen. Bei signed Variablen lauten die entsprechenden Befehle IMUL bzw. IDIV. Dadurch kann der Prozessor erst Integer (also signed) und Word (unsigned) unterscheiden. Außerdem kann es bei der Multiplikation zu einem Overflow (zu deutsch: Überlauf) kommen. Das ist eine Situation, die eintritt, wenn das Ergebnis größer als die Registerbreite (in diesem Fall 65535) ist. Dann wird zu dem Register AX noch das Register DX zur Speicherung des Ergebnisses verwendet (der Prozessor macht dies automatisch). Sie müßten also das Loword einer Longint-Variable (untere 2 Bytes) mit dem Inhalt des Register AX füllen und den Rest, das Hiword (die anderen 2 Bytes) mit dem Inhalt des Register DX überschreiben um das komplette Ergebnis einer Multiplikation von Integer-Variablen in einer Long Integer Variablen zu sichern.
Wo befindet sich der aktuelle Programmcode (CS), die aktuellen Programmdaten (DS), der Zwischenspeicher (SS) und das wahlfreie Segment (ES) im Arbeitsspeicher? All diese Fragen werden mit Hilfe der Segmentregister festgelegt. Doch vorsichtigt falsche Werte in den Registern führen unweigerlich zum Absturz. Zur Programmierung von Assemblerteilen innerhalb von Pascal sind nur DS, ES interessant. Die anderen werden automatisch vom Compiler eingestellt und brauchen uns deshalb nicht mehr zu interessieren. Hier sehen sie nun alle Segmentregister mit den dazugehörenden Offsetregistern. Registerpaare:
Das Register CS bildet zusammen mit dem Register IP einen Zeiger auf den aktuellen Befehl. Nach jedem Befehl erhöht die Prozessor automatisch IP um die entsprechenden Bytes, um den nächsten Befehl abzuarbeiten. Dabei enthält CS den Wert, der das Segment bestimmt, und IP den Offsetwert. Auch DS (Segmentanteil) und SI (Offset) bilden zusammen einen Pointer, der auf die aktuelle "Variable" zeigt. Das BP Register dient zur vereinfachten Adressierung von Variablen wie z. B. Arrays.
Besondere Beachtung sollte dem Register DS geschenkt werden, da vor jeder Veränderung der alte Inhalt gerettet und nach beenden des entsprechenden Programmteils wieder hergestellt werden muß (betrifft auch CS, ES braucht nicht gesichert werden), denn sonst entsteht sehr schnell eine Situation, wo man sagt der Computer ist abgestürzt. Mit anderen Worten, die CPU arbeitet z. B. ohne es zu wissen, ein anderes Programm mit den falschen Variablen ab. Wobei die falschen Variablenwerte schnell zu Schutzverletzungen, falscher Befehlsausführungen usw. führen. Dann ist auch der Weg zu speziellen Fehlerabfangroutinen (Aufruf aufgrund fehlerhafter Befehlsinterpretation) des Prozessors nicht mehr weit, was schließlich zu guter Letzt den Stillstand des Systems bedeutet.
Die CPU-Register sind beim Zwischenspeichern von Ergebnissen/Werten schnell erschöpft. Deshalb benötigt man einen Zwischenspeicher, den Stack. Dort werden Parameter von Prozeduren/Funktionen, Funktionsrückgaben oder Registerinhalte gespeichert. Der Befehl PUSH AX (Stack schreiben) rettet den Inhalt (2 Bytes) von AX auf den Stack (SP wird um 2 erhöht). POP AX (Stack lesen) holt 2 Bytes vom Stack in das Register AX (SP wird um 2 vermindert). Das Auswahl des zu lesenden bzw. zu schreibenden Element erfolgt nach dem sogenannten LIFO-Prinzip (Last in/First out). Es wird also immer am Ende gelesen bzw. geschrieben. Außerdem sollte man folgendes beachten: Auch hier gibt es einen Overflow, der aber bei überlegter Programmierung nicht eintreffen wird. Der Stacküberlauf wird durch entsprechend große Reservierung (wird in der Regel vom Compiler eingestellt) nur selten erreicht.
Das Flagregister: Dieses Register speichert keine Zeiger oder Variablen, sondern einfach nur CPU-Zustände. Das Register ist 1 Byte groß, wobei jedes dieser 8 Bit für eine andere Aufgabe benutzt wird. Diese Register sind entscheidend bei wichtigen Operationen und können wenn nötig auch auf den Stack zwischengespeichert werden.
Das Carry-und das Overflow-Flag werden beim Überlauf und beim Übertragen in einer anderes Register gesetzt. Z. B. bei der Multiplikation zweier Wordvariablen kann ein 16bit-Register zu klein sein. Z. B. bei 1000*2000 ist das Ergebnis schon größer als 65535, es kommt zum Overflow (zu deutsch: Überlauf). Die restlichen zu vielen Bits des Ergebnisses werden in ein anderes Register übertragen und das Carry-Flag (zu deutsch: Übertrag) wird gesetzt. Nur mit ihrer Hilfe kann das Programm auf solche Situationen reagieren und eventuelle Fehlerabfangroutinen ausführen lassen. Das Direction-Flag legt die Kopierrichtung von (auch großen) Speicherbereichen fest. Entweder wird nach jeder Kopieraktion das entsprechende Register inkrementiert oder dekrementiert. Standardmäßig ist das Bit gesetzt. Wenn man z. B. ein Bild auf dem Kopf darstellen will, muß lediglich die Kopierrichtung geändert werden (und dann kopiert werden) und schon steht das Bild verkehrt herum. Wenn das Interrupt-Flag gelöscht ist werden keine Interrupts von der CPU bearbeitet. Durch das Löschen kann man dann bei kritischen Operationen Unterbrechungen durch fremde Programme Unterbinden und in aller Ruhe seinen Programmteil ausführen lassen. Danach muß aber sofort das Flag wieder gesetzt werden. Standardmäßig ist selbstverständlich das Interrupt-Flag gesetzt. Das Carry-Flag hat im Zusammenhang mit dem Signed-, Zero- und Parity-Flag eine sehr wichtige Aufgabe. Nur durch den Vergleich mit diesen Flags kann die CPU bedingte Sprunganweisungen durchführen.
Der Datentyp Pointer (zu deutsch: Zeiger) dient zur Markierung bzw. Reservierung
großer Speicherbereiche im RAM. Er besteht aus zwei Bestandteilen, dem Segmentanteil und dem Offsetanteil.
Der Offsetanteil gibt die relative Position zum Segmentanteil an, ist sozusagen die Unteradresse. Man kann also
mit Hilfe der Formel Segmentanteil * 16 + Offsetanteil die physikalische Adresse berrechnen. Das
Ergebnis interpretiert der Prozessor als 20bit Zahl, mit der die Speicherstelle im RAM identifiziert wird (entspricht
dem Realmode). Z. B. ergibt der Segmentanteil A000h und der Offset 0 die Speicheradresse 655360. Die Adresse erhält
man auch mit einem Segmentanteil von 963Ch und Offsetanteil von 9C40h (963Ch * 16 + 9C40h = 655360). Die Speicheradressen
lassen sich also mit vielen Kombinationen aus Segment und Offset darstellen. Leider können im Realmode keine
größeren zusammenhängende Speicherbereiche als 64 KB verwalten werden, was dagegen schon im
16bit-Protected Mode möglich ist. Doch gibt es auch dort das schwerwiegende Problem der Segmentierung. Sobald
ein Speicherbereich größer 64 KB ist, müssen nämlich spezielle Routinen zur Auswahl des
richtigen Segments implementiert werden. Mit anderen Worten bei einer Operation mit dem RAM müssen dann ständig
die Segment - und Offsetregister aktualisiert werden, was einen erheblichen Programmier- und Zeitaufwand erfordert.
Ab dem 80286 PC beherrscht der Prozessor neben den gewöhnlichen Betriebsmodus (Realmode) einen neuen Modus, den Protected Mode. Das Betriebssystem und die Programme sind nun besser untereinander vor Überschreibungen geschützt und die Speichergrenze (max. adressierbare Speicherstelle) von nur 1 MB wird überwunden. Man kann jetzt bis zu 16 MB adressieren, da der Segmentanteil (im Protected Mode = Selektor) einer Adresse nun zusammen mit dem Offset eine 24bit großen Adressraum erlaubt. Der Protected Mode (zu deutsch: geschützter Modus) ermöglicht auch ein komplexes Verwaltungssystem. So zeigen die Werte in den Segmentregistern (enthält den Selektor) nicht mehr direkt auf die physikalische Adresse, sondern bestimmen den zuständigen Eintrag in einer Beschreibertabelle (descriptor table). Dort sind alle wichtigen Angaben über jedes Segment festgelegt. Die Zugriffsrechte (darf gelesen/geschrieben werden), die Größe des reservierten Speicherblockes und andere Eigenschaften (ist das Segment verschiebbar oder fixiert) und vor allem die eigentliche Basisadresse werden dort gespeichert. Das bedeutet bei irgendwelchen Schreib- oder Leseaktionen eines Programms, überprüft das Betriebssystem mit Hilfe der CPU, ob die betroffenen Arbeitsspeicherbereiche dem Programm gehören oder nicht. Bei Unzulässigkeit wird dann einfach der Übeltäter (z. B. ein fehlerhaftes Programm) terminiert. Natürlich entstehen durch solche Programme dann nur noch selten Systemabstürze (siehe Linux, OS/2 oder Windows NT). Jedoch bietet diese Art der Speicherverwaltung nicht nur Vorteile. Der Nachteil liegt in der zeitraubenden Speicherverwaltung, z. B. dauert das Laden eines Pointers erheblich länger als im Realmode. Doch hat sich der Protected Mode in allen Softwarebereichen durchgesetzt. Besonders die neuere 32bit-Protected-Mode-Version wird fast überall eingesetzt (auch bei den Computerspielen). Denn hier können sage und schreibe bis zu 4 GB physikalischer Speicher adressiert werden, was den Speicherhunger aller komplexer Anwendungen endlich zufriedenstellen kann. Natürlich braucht man dafür genug physikalischen RAM. Außerdem gibt es durch die 32bit-Adressierung keine Notwendigkeit für besondere Routinen zur Umgehung der Segmentgrenzen, denn es gibt sie, die 64 KB-Blockunterteilung, einfach nicht mehr. Linux, OS/2, Windows 95 und Windows NT sind mehrere moderne Betriebssysteme, die alle diese Vorteile ausnutzen (auch das preemtive Multitasking). Deshalb wird irgendwann MS DOS, geschrieben für den Realmode, und dessen Komplementär Windows 3.1 (benutzt 16bit-Protected-Mode, kooperatives Multitasking) vollständig von moderneren Betriebssystemen ersetzt werden. Hier eine kurze laienhafte Unterscheidung zwischen preemtives und kooperatives Multitasking. Beide erlauben Multitasking, wobei das preemtive kurz gesagt das "echte" bezeichnet. Dort bestimmt das Betriebssystem durch verschiedene Verfahren den Anteil eines jeden Programmes an der Prozessorzeit. Das Multitasking wird also aktiv durchgeführt und wird bei hoher CPU-Auslastung einfach erzwungen. Hingegen herrscht beim kooperativen, unechtem Multitasking, eine Desperado-Mentalität. Wer zuerst kommt malt zu erst. So ungefähr kann man das Verfahren der freiwilligen Zeitvergabe durch die Programme beschreiben. Das Betriebssystem fragt die Programme einzeln an und vergibt die von ihnen zugewiesene Zeiten an andere Programme.
Wenn Sie einen größeren Speicherbereich z. B. für eine Bilddatei benötigen, brauchen Sie nur eine Variable vom Typ Pointer. Mit der Funktion GlobalAllocPtr ( Eigenschaften , Speicherbereichgröße ) reservieren Sie einen Speicherbereich beliebiger Größe. Die Rückgabewert ist ein Pointer, der auf Anfang des neu reservierten Speicherbereiches zeigt. Die Eigenschaften legen die genauere Behandlung des Speicherbereiches fest, man kann also mit GMEM_MOVEABLE verschiebbare und mit GMEM_FIXED feste Speicherblöcke reservieren. Bei verschiebbar wird dem Betriebssystem bei Bedarf erlaubt, die Basisadresse des Selektors zu ändern, was bei Speicherknappheit sehr vorteilhaft sein ist. GMEM_FIXED dagegen, reserviert einen festen Speicherblock, den das Betriebssystem nicht innerhalb des Speichers verschieben wird. Anders als im Real Mode kann hier sogar eine Speicherbereichsgröße über 64 KB reserviert werden. Der Protected Mode Handler reserviert dann einfach mehrere miteinander verbundene Segmente. Trotzdem kann wie bereits erwähnt nicht komplett auf einmal auf den Speicher zugegriffen werden. Aufgrund der Segmentgrenzen kann der großen Speicher nur mit ein paar Tricks genutzt werden. Jedoch erst später dazu mehr. Hier wird z. B. ein verschiebbarer 1 Mio. Bytes großer Speicherbereich reserviert: VAR Zeiger_auf_Speicherbereich : POINTER; BEGIN
Die von Pascal zur Verfügung gestellten Befehle zum Kopieren Move ( Quellpointer , Zielpointer ) und zum konstantem Füllen Fillchar ( Pointer , Speicherbereichsgröße , Füllbyte ) sind für die Grafikprogrammierung wenig geeignet, da sie einfach zu langsam ausgeführt werden. Zum Vergleich: Bei der direkten Assemblerprogrammierung wird beides um einige mal schneller ausgeführt (Kopieren 4x und Füllen 2x so schnell). Denn hier nutzt man einfach die 4 Byte bzw. 2 Byte Schreibgröße und nicht wie Pascal lediglich die 1 Byte Größe. Nach der Nutzung eines Speicherbereiches sollte dieser so schnell wie möglich dem System zurückgegeben werden, das erledigt die Funktion GlobalFreePtr ( Pointer ). Sie gibt einen reservierten Speicherbereich dem Betriebssystem zurück. Der Pointer ist nun ungültig und darf nicht ohne vorherige Überschreibung des Pointerinhaltes (z. B. durch Ptr) benutzt werden. Achtung, ungültige Pointer führen zum Abbruch des Programms und oft auch zum Systemstillstand. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2. VGA/SVGA-Programmierung Bei der Grafikprogrammierung ist vor allem ein Gesichtspunkt sehr wichtig: die Geschwindigkeit. Alle Grafikoperationen sind sehr aufwendig und verlangen sehr viel Prozessorzeit, um die Schnelligkeit von vornherein durch so wenig Maschinenbefehle wie möglich zu erreichen, müssen die entsprechenden Programmteile deshalb in Assemblersprache implementiert werden. Die Grafikkarte Ihres PCs kennt verschiedene Bildschirmmodi, in denen die Anzahl der Pixel und Farben unterschiedlich sind. Der Standardbildschirmmodus mit einer Bildbreite von 320 Pixeln und einer Bildhöhe von 200 Pixeln (gekennzeichnet mit der Nr. 13H) war der erste mit 256 Farben. Hier entspricht jeder Pixel einem Byte im Arbeitsspeicher Ihres PCs. Dadurch ergeben sich auch die 256 Farben, denn 28 entspricht einem Byte, welches 256 verschiedene Zustände darstellen kann. Dieser Modus ist so einfach aufgebaut (jeder Pixel belegt ein Byte), daß die Programmierung enorm erleichtert wird. Zur Verständnis der späteren Programmteile ist es sehr wichtig, daß Sie den Aufbau der Bildspeicherdaten im RAM verstehen. Der Arbeitsspeicher ist in unterschiedliche Abschnitte, Segmente genannt, unterteilt. Das Segment A000H enthält die Bildspeicherdaten (diese entsprechen den Pixeln auf den Bildschirm). Man muß also nur noch die Bytes dort verändern und schon sieht man das Ergebnis auf dem Bildschirm. Das Koordinatensystem fängt links oben in der Ecke an. Dort ist der Punkt 0,0 (X,Y) angelegt. Eine bestimmte Adresse, Segmentanteil A000h und Offsetanteil 0, betrifft diesen Punkt. Wenn nun der Bytewert an dieser Position verändert wird, erscheint automatisch eine andere Farbe auf dem Bildschirm. Um nun jeden einzelnen Punkt zu erreichen, kann man die Adresse auch so formulieren: Segmentanteil ist A000h, Offsetanteil wird durch 320*Y+X berechnet. Möchten Sie z. B. dem Punkt ganz rechts unten in der Ecke eine andere Farbe geben, muß das Segmentregister mit A000h geladen und in das Offsetregister die Zahl 64000 (320*199+320) geschrieben werden. Anschließend muß der eigentliche Schreibbefehl programmiert werden und fertig ist die Farbänderung eines Punktes. Adreßbeispiele:
Das ist doch gar nicht so schwer werden Sie sagen, aber wie werden die Farben eigentlich genau festgelegt? Die Farbvergabe erfolgt durch eine Tabelle, dem Palettenregister. Dort werden 256 verschiedene Tabelleneinträge mit jeweils einem roten, grünen und blauen Farbanteil (RGB-Farben) gespeichert. Jeder Farbanteil ist ein Byte groß, was einen Wertebereich jeweils für Rot, Grün und Blau von 0 bis 255 ermöglicht. Die Bytewerte beziehen sich also auf die einzelnen Farbeinträge dieser Palette und ergeben erst zusammen die vielen bunten Farben auf dem Bildschirm. Enthält der 0. Farbeintrag (gezählt wird von 0 bis 255) einen Rotanteil von 255, einen Grünanteil von 0 und einen Blauanteil von 0, definiert dieser eine sehr helle rote Farbe. Schreiben Sie nun an der Position 0,0 den Bytewert 0, erscheint automatisch ein Pixel mit einer sehr hellen roten Farbe. Die einzelnen Bytes der Bildspeicherdaten verweisen also auf die Paletteneinträge, in denen die RGB-Farben definiert werden. Man kann natürlich die Farbregister mit beliebigen Rot-,Grün- und Blauwerten laden, so daß aus einem Farbtopf von 262.144 Farben (ergibt sich aus den RGB-Bytes=218 jeweils 26 für jeden Farbanteil), 256 Farben gleichzeitig dargestellt werden können. Gegenüber dem Vorgänger, der EGA-Karte war dies ein erheblicher Leistungssprung hinsichtlich der Programmierung und Darstellung von Grafiken. Auch deswegen verhalf die Einführung der VGA-Karte den PC-Spielen zu einem starken Höhenflug. Grafikmodus einschalten Nach Theorie kommen wir nun zur Praxis. Man muß natürlich zuerst den richtigen Grafikmodus einschalten. Erreichen kann man dies durch einen bestimmten BIOS-Aufruf. Das BIOS stellt eine unabhängige Hardware-Schnittstelle dar und wird deshalb der zweiten Möglichkeit, der direkten Hardware-Programmierung, fast immer vorgezogen. Folgender Programmteil schaltet den Grafikmodus 13H ein: MOV AH, 00H ‘ Legt die BIOS-Funktion "Bildschirmmodus einstellen" fest MOV AL, 13H ‘ Nr. des einzustellenden Grafikmodus INT 10H ‘ Einsatz des BIOS durch Ausführung des Interrupts 10H Pixel lesen/schreiben Nach der Ausführung der drei Programmzeilen sieht man erstmal gar nichts außer eine schwarzen Bildschirm, deshalb lassen wir einen Punkt an einer beliebiger Stelle auf dem Bildschirm erscheinen. Das erledigt eine kleine Prozedur: Procedure Putpixel ( x , y : Integer ; Pixelfarbe : Byte ); Assembler; asm mov ax,320 (* Bildbreite ist 320 Bytes *) imul y (* Register AX mit Y multiplizieren *) add ax,x (* X zu Register AX addieren *) mov es,segA000 (* Bildspeichersegment auswählen *) mov di,ax (* Ergebnis von AX als Offset angeben *) mov al,Pixelfarbe (* Pixelfarbe im Teilregister AL speichern *) stosb (* Pixel in den Arbeitsspeicher schreiben *) End; Beim Lesen eines Pixels sind nur ein paar Schritte mehr notwendig: Function GetPixel ( X , Y : Integer ) : Byte; assembler; asm push ds (* DS auf den Stack retten *) mov ax,320 (* Bildbreite ist 320 Bytes *) imul y (* Register AX mit Y multiplizieren *) add ax,x (* X zu Register AX addieren *) mov ds,segA000 (* Bildspeichersegment auswählen *) mov si,ax (* Ergebnis von AX als Offset angeben *) lodsb (* Pixel aus dem RAM laden *) pop ds (* geretteten Inhalt vom Stack in DS schreiben *) push ax (* Pixelfarbe als Ergebnisrückgabe retten *) end; Linie zeichnen Um eine Linie zu zeichnen muß man die logische Linie auf das Pixelraster legen. Dabei muß man sich ein rechtwinkeliges Dreieck vorstellen, welches zwei Seiten als Höhe und Breite besitzt, wobei die gegenüberliegende Seite die eigentlich zu zeichnende Linie ist. Mit Hilfe der Division in Bezug zu der Höhe und der Breite kann man nun Pixel für Pixel der Linie zeichnen. Procedure Line ( x1 , y1 , x2 , y2 : integer ; PixelFARBE : Byte ); var dx1 , dy1 , xa , ya : integer; n , maxn : word; begin asm mov ax,x2 (*dx1:=x2-x1; Breite berechnen *) sub ax,x1 mov dx1,ax mov ax,y2 (*dy1:=y2-y1; Höhe berechnen *) sub ax,y1 mov dy1,ax end; (* Breite oder Höhe größer? *) maxn:=abs(dx1); if maxn<abs(dy1) then maxn:=abs(dy1); if maxn=0 then inc(maxn); for n:=0 to maxn-1 do begin asm mov ax,dx1 (*xa:=x1+(dx1*n)div maxn;*) imul n idiv maxn add ax,x1 mov xa,ax mov ax,dy1 (*ya:=y1+(dy1*n)div maxn;*) imul n idiv maxn add ax,y1 mov ya,ax (* Folgendes verhindert zeichnen über die Ränder *) cmp xa,319 ja @verdeckt cmp xa,0 jb @verdeckt cmp ya,199 ja @verdeckt cmp ya,0 jb @verdeckt (* Offset berechnen *) mov ax,320 imul ya add ax,xa (*scroff:=ya*320+xa;*) mov es,sega000 (* Bilddatensegment auswählen *) mov di,ax (* richtigen Offset angeben *) mov al,pixelFARBE (* zu zeichnende Farbe festlegen *) stosb (* Pixel schreiben *) @verdeckt: end; end; end; Rechteck zeichnen Ähnlich funktioniert das Zeichnen eines gefüllten Rechteckes. Es wird aber nicht jeder Pixel vom Anfang bis zum Ende einer Zeile einzeln geschrieben, sondern jede Zeile komplett mit nur einem Befehl. Logischerweise erhöht sich dadurch die Ausführungsgeschwindigkeit beträchtlich. PROCEDURE Bar ( X , Y , Breite , Hoehe : Integer ; Pixelfarbe : Byte ); VAR Zeile : Integer; Laenge : Integer; BEGIN Laenge : = Breite DIV 2; (* Schreiblänge ist die Hälfte, da mit 16-bit gefüllt wird *) FOR Zeile : = Y TO Y + Hoehe DO (* von der ersten bis zur letzten Zeile wiederholen *) BEGIN ASM mov ax,320 imul Zeile add ax,x (* Offsetberechnung *) mov di,ax (* Offset laden*) mov al,Pixelfarbe (* Farbe in Unterregister AL laden *) mov ah,al (* Farbe auch ins andere 8-bit-Unterregister laden *) mov cx,laenge (* Länge der Schreibaktion festlegen *) mov es,SegA000 (* Bildspeichersegment auswählen *) rep stosw (* Zeile mit 16-bit-Schreibaktion füllen *) END; END; END; Bild/Grafik anzeigen Das Laden eines Bildes/Grafik ist Gegensatz zu dem Vorhergehendem viel komplexer. Zuerst muß das gewünschte Bild, welches sich in einer Bilddatei befindet in den Arbeitsspeicher geladen werden. Dann müssen die Informationen der Bilddatei (Größe, Breite, Höhe etc.) ausgewertet und die Farbpalette neu eingestellt werden. Danach werden nur noch die eigentlichen Bilddaten in das Segment A000H an die richtige Stelle kopiert und es erscheint das Bild. Hier wird zuerst einmal der Dateiheader einer Grafik-BMP-Datei definiert: type bmp_header_ = record signatur : word; flen : longint; dummy1 : word; dummy2 : word; offset : longint; info_size : longint; xmax : longint; ymax : longint; polanes : word; bits_per_pixel : word; compress : word; xsize : longint; hdpi : longint; vdpi : longint; cols : longint; coli : longint; end; var bmp_header : bmp_header_; (* Dateiheader *) pal_array : array[0..255,0..2]of byte; (* Palettenfarben mit Rot-, Grün- und Blauanteilen *) pal_res : array[0..255]of byte; (* muß mitgeführt werden *) helligkeit : integer; (* speichert die Helligkeit der Farben *) Nach dem Laden der Headerinformationen kann nun die neue Farbpalette eingestellt werden: procedure setpalette ( anzahl : integer ); var merker,i : integer; pl_array : array[0..255,0..2]of byte; begin merker : = helligkeit; helligkeit : = helligkeit*2; for i : = 0 to anzahl do begin pl_array[i,red] : = (pal_array[i,blue]*helligkeit div 255) shr 2; pl_array[i,green] : = (pal_array[i,green]*helligkeit div 255) shr 2; pl_array[i,blue] : = (pal_array[i,red]*helligkeit div 255) shr 2; end; port[$3C8] : = 0; for i : = 0 to anzahl do begin port[$3C9] : = pl_array[i,red]; port[$3C9] : = pl_array[i,green]; port[$3C9] : = pl_array[i,blue]; end; helligkeit : = merker; end; Folgender letzter Programmteil zeigt wie die Bilddatei geladen, die gezeigten Prozeduren genutzt und die Bilddaten kopiert werden. Function LoadBMPFile(x , y: Integer; Filename: String):Boolean; var Handle:file; Bildgroesse:longint; i,it:integer; ts2,to2,tsegmentBMP,toffsetBMP,tsegment,toffset:word; groesse2:word; breite,hoehe:integer; (* ermittelt den richtigen Pointer abhängig vom Offset *) function getptr(p:pointer;offset:longint):pointer; type long=record lo,hi:word; end; begin getptr:=ptr(long(p).hi+long(offset).hi*selectorinc,long(p).lo+long(offset).lo); end; (* lädt die eigentlichen Bilddaten in den Arbeitsspeicher *) Function loadfile:pointer; var buffer:pointer; size,offset,count:longint; begin buffer:=nil; size:=filesize(handle)-filepos(handle); buffer:=globalallocptr(gmem_moveable,size); if buffer<>nil then begin offset:=0; while offset<size do begin count:=size-offset; if count>32768 then count:=32768; blockread(handle,getptr(buffer,offset)^,count); inc(offset,count); end; end; close(handle); loadfile:=buffer; end; (* wird ganz am Schluß aufgerufen und zeigt die Grafik an *) procedure putVGAPixel(x1,y1,breite,hoehe:integer; tsegment,tsegmentBMP,toffsetBMP:word); const bildbreite=320; var c2,toffset:word; zaehler,y,x2:integer; begin (* Korrektur der Breite auf ein vielfaches von 4 *) if breite mod 4<>0 then breite:=breite+(4-(breite mod 4)); x2:=breite; (* nach dem rechten Rand abschneiden *) if 320-x1<=breite then x2:=320-x1; zaehler:=x2 div 4; for y:=y1 to y1+hoehe-1 do begin c2:=toffsetbmp+breite*(y-y1); toffset:=bildbreite*y+x1; asm push ds mov ds,tsegmentbmp (* Quelladresse-Segment *) mov si,c2 (* Quelladresse-Offset *) mov cx,zaehler mov es,tsegment (* Zieladresse-Segment *) mov di,toffset (* Zieladresse-Offset *) db 66h;rep movsw (* 32-bit-Kopieraktion *) pop ds end; end; end; (* Programm der Hauptfunktion, hier werden der Dateiheader ausgewertet und die Unterfunktionen aufgerufen *) begin assign(handle,filename); reset(handle,1); blockread(handle,bmp_header,54); if (lobyte(bmp_header.signatur)<>$42)Or(hibyte(bmp_header.signatur)<>$4D)then begin close(handle); exit; end; for it:=1 to 256 do begin blockread(handle,pal_array[it-1,0],3); blockread(handle,pal_res[it-1],1); end; filenamemerker:=filename; bildgroesse:=bmp_header.xmax*bmp_header.ymax; po:=loadfile; tsegment:=fenstersegment; tsegmentBMP:=seg(po^); toffsetBMP:=ofs(po^); setpalette(255); breite:=bmp_header.xmax; hoehe:=bmp_header.ymax; if bildgroesse<=65535 then begin putVGAPixel(x,y,breite,hoehe,segA000,tsegmentBMP,toffsetBMP); End; globalfreeptr(po); end; |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3. Wichtige Assembler-Befehle
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
4. Wichtige CPU-Register (x86-kompatibel)
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
5. Wichtige Übersichten
Flags-Register - Bitbedeutung
Datentypen - Wertebereiche
Datentypen - Speicherung der einzelnen Bytes/Bits
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(* Referat: CPU/Assembler 1997 Autor: Bernd Noetscher ----------------------------------------------------------- Geschwindigkeitsvergleich von Maschinensprachen (Assembler) und Hochsprachen *) VAR REPEATER, X, Y : WORD; BEGIN ASM mov ax,13H int 10h (* VGA einschalten: 320*200 Pixel *) @REPEATER_SCHLEIFE: @Y_SCHLEIFE: @X_SCHLEIFE: mov ax,0a000h (* AX=0a000H *) mov es,ax (* ES=AX *) mov ax,320 (* AX=320 *) mul y (* AX=AX*Y *) add ax,x (* AX=AX+X *) mov di,ax (* DI=AX *) mov ax,y (* AX=X *) add ax,repeater (* AX=AX+REPEATER *) stosb (* Inhalt von AL nach ES:DI *) inc x (* X=X+1 *) cmp x,319 (* X kleiner 319 ? *) JBE @X_SCHLEIFE (* ja, dann gehe zu X_SCHLEIFE *) mov x,0 (* X=0 *) inc y (* Y=Y+1 *) cmp y,199 (* Y kleiner 199 ? *) JBE @Y_SCHLEIFE (* ja, dann gehe zu Y_SCHLEIFE *) mov x,0 (* X=0 *) mov y,0 (* Y=0 *) inc repeater (* REPEATER=REPEATER+1 *) cmp repeater,100 (* repeater kleiner 100 ? *) JBE @REPEATER_SCHLEIFE (* ja, dann gehe zu REPEATER_SCHLEIFE *) mov ax,03H int 10h (* Textmodus einschalten: 80*25 Zeichen *) END; END. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<< Zurück Alle Angaben ohne Gewähr. Benutzung auf eigenes Risiko. (C) Copyright Bernd Noetscher 1995 - 2000 eMail: webmaster@berndnoetscher.de |