9TH OF FEBRUAR 1994 - THE IMMORTAL SOULS ARE VERY PROUD TO PRESENT: BODY BLOWS VERSION 2 !!! TEAM 17 MADE THREE 983040 BYTES DISKS (USUALLY 901120 BYTES) BUT THAT DID'NT MATTER TO ME. I DECRUNCHED ALL THE DATAS AND RECRUNCHED THEM WITH THE UNBELIEVABLE CRUNCHMANIA AND NOW I HAVE 380KB FREE DISK SPACE ! I WAS REALLY CLOSE TO PUT MILLENIUM (THE GAME!) ON THE EMPTY SPACE BUT THERE WERE 3 BLOCKS TO LESS EMPTY - BAD LUCK ! BUT NOW THE CREDITS: THE CRACK WAS DONE BY MYSELF (NAGILUM) OF COURSE, I ALSO CODED THE INTRO. THE LOGO AND THIS FONT WERE DRAWN - AS USUALLY - BY ASGARD AND THE NICE SOUND YOU SHOULD HEAR RIGHT NOW WAS DONE BY TRAY! I'M QUITE SURE THAT THIS INTRO HAS THE LONGEST SCROLLTEXT EVER WRITTEN (ABOUT 129KB)! THE TEXT IN THE MIDDLE OF YOUR SCREEN IS A COMPLETE BOOK CALLED `ASSEMBLER - PROFI TIPS'N TRICKS". YOU CAN STOP THE TEXT BY HOLDING THE RIGHT MOUSEBUTTON. THE SOURCEFILE FOR THIS INTRO TAKES 300KB BUT ONLY 500 OF 6000 LINES ARE REAL CODE THE REST ARE THE COPPERLIST, FONTS AND THE SCROLLTEXTS! SHORT AND PC-RELATIVE IS THE WAY I LIKE TO CODE !!! § ... ... ... WHAT THE HELL, TRAY IS HERE AGAIN TYPING SUM REALLY HOT INFORMATION FOR YA! YEAH, I KNOW THAT IT IS STANDING IN LITTLE (OR MAYBE BIG, EH?) INTROS FROM TIS BUT I THINK YOU SHOULD BETTER KNOW ABOUT IT ... IT IS THE SOUNDPACK WHICH I PRODUCED IN ONE AND A HALF YEAR. IF U WANT IT - CONTACT ME (TRAY) UNDER DA FOLLOWING ADDRESS ... IVO KUECHLER, PROF.-WILLKOMM-STR. 3A, 09212 LIMBACH-OBERFROHNA, GERMANY. IF YA WANNA GET SUM DEMOS U CAN CONTACT ME, TOO. IF YA WANT TO SEND ME SUM STUFF REMEMBER ... NO ILLEGAL STUFF!!! ... WELL THAT IS ALL I WANNA TELL YA! ... ... ... NAGILUM AGAIN TYPING HERE! SEE 'YA IN OUR NEXT PRODUCTION. I GOT A LOT OF STUFF RIGHT HERE (TURRICAN 3, ELITE 2, DISPOSABLE HERO, URIDIUM, ZOOL II,...) I THINK MORTAL KOMBAT'LL BE THE NEXT WHILE TRAY IS DOING `DIE SIEDLER". LET'S SEE WHAT'S NEXT! BYE ! § TEXT WRAP Press right mousebutton to stop the text... The following book was written by Manfred Leidorf To order the book write to: media Verlagsgesellschaft Waldweg 5 88175 Scheidegg Germany or INTERCOMP A. Mayer G.m.b.H Heldendankstraße 24 A-6900 Bregenz It costs DM 39.90 or ÖS 329.- or SFR 39.90 and if you order it you'll get all sources on disk ! But let's start right now and here: Wollten Sie schon immer mal erfahren, wie man eine Library selbst erstellt? Möchten Sie endlich erfahren, wie das AA-Chipset direkt programmiert wird ? Diese und viele andere Fragen beantwortet "Assembler Profitips & Tricks." Wichtige Kniffe zum optimalen Programmieren gehören ebenso dazu wie eine Einführung in die Prozessoren ab 68020 und dem mathematischen Koprozessor. Eine "Frage- und Antwort-Runde" und die Grundlagen für einen absolut perfekten Kopierschutz, der selbst Hardwarezusätzen standhält, runden das Ganze angenehm ab. * AA-Chipset-Programmierung * Erweitere Grafikfähigkeiten unter OS 3.0 * Kopierschutz * Optimierter Programmcode * Fragen und Antworten * Wie erstelle ich eine eigene Library ? INHALT Einführung................................xxxx Kapitel 1: Die höheren Prozessoren...................xxxx Kapitel 2: Grundlagen für optimiertes Programmieren..xxxx Kapitel 3: OS 2.0 kontra OS 3.0 (Grafik).............xxxx Kapitel 4: Blitter speziell und AA-Chipset...........xxxx Kapitel 5: Eine Library im Eigenbau..................xxxx Kapitel 6: Fragen und Antworten......................xxxx Kapitel 7: Der absolut sichere Kopierschutz..........xxxx Anhang....................................xxxx Einführung Willkommen bei "Profi-Tips und Tricks in Assembler". Obwohl Hochsprachen zur Zeit eine Hochkonjunktur erleben, ist Assembler immer noch eine sehr beliebte Programmiersprache. Da es die Sprache des Prozessors selbst ist, lassen sich - allen anderweitigen Behauptungen zum Trotz - nur so die optimalen Möglichkeiten aus dem Rechner kitzeln. Dieses Büchlein soll Ihnen helfen, ein paar Feinheiten der Maschinensprache kennenzulernen, und setzt entsprechend vorraus, daß Sie bereits mindestens Grundkenntnisse dieser Sprache besitzen. Natürlich kann ein solch kompaktes Buch bei weitem nicht alles abdecken. Deshalb haben wir ein paar ganz spezielle Themen ausgesucht, die oft in Form von Leserfragen auftauchen. Diese Buchreihe könnte - ihren WÜnschen und Vorschlägen entsprechend - durchaus fortgesetzt werden, auch mit eventuell größerem Umfang oder ganz speziellen Themen. Wenn Sie Wünsche oder Vorschläge haben: Schreiben Sie uns! Die Welt des Programmierens ist unerschöpflich und selbst ein 1000-Seiten-Buch hätte nicht die Möglichkeit, alles zu berücksichtigen. (Nichts destotrotz habe ich auch ein solches geplant....) Aber ich habe mich umgehört. Was interessiert einen angehenden bzw. gerade lernenden Assemblerprogrammierer speziell auf dem Amiga? Welche Fragen tauchen am häufigsten auf? Folgendes kam dabei heraus: Die zur Zeit aktuellsten Themen - Prozessoren ab 68020 - Programmierung des AA-Chipsets - Optimiertes Programmieren - Erstellen einer eigenen Library Dies ist quasi die "Top 4" der Wünsche, die deshalb in diesem Buch besondere Beachtung finden. Dokumentationen der AA-Chips und OS 3.0 sind zur Zeit noch sehr spärlich, von den "Autodocs" in Englisch einmal abgesehen, und selbst dort gibt es einige Verwirrungen. Zusätzlich gibt es ein kleines "Frage und Antwort" - Spiel und sonstige Feinheiten. Noch einige gute Ratschläge zu Beginn: 1.) So schön Assembler auch ist, der Nachteil bleibt die große Fehleranfälligkeit, wenn keine Programmierdisziplin gewahrt wird. Eine Hochsprache zu beherrschen ist deshalb sicher nicht verkehrt. So kann das Programmgerüst wesentlich schneller und komfortabler erstellt werden. Die zeitkritischen Teile schreiben Sie dann einfach in Assembler. Moderne Compiler ermöglichen das Einbinden von Assemblerteilen. Es ist also nicht unbedingt notwendig, eine Dateiverwaltung in reinem Assembler zu schreiben. Hier würde es beispielsweise genügen, das Hauptprogramm zum Beispiel in C oder Modula II zu schreiben, während zeitkritische Sortierroutinen in Assembler erstellt werden. 2.) Besorgen Sie sich (bitte auf legale Weise) unbedingt die offiziellen Includedateien zu OS 2.0 bzw. OS 3.0. Benutzen Sie einen Assembler, der mit solchen Includes auch arbeiten kann. Auch wenn es am Anfang sehr gewöhnungsbedürftig ist: Es ist doch sicher aussagekräftiger, "move.b" bm_depth(a2),d0" zu schreiben als "move.b 5(a2),d0". Ein weiterer Vorteil: Wenn sich irgendwelche Offsets in einer neuen Betriebssystem-Version ändern, brauchen Sie nur Ihr Programm mit den neuen Includes OHNE ÄNDERUNG zu assemblieren - schon ist ihr Werk angepaßt! 3.) Das allseits beliebte "Hardwarehacken" ist sicher in einigen Situationen sinnvoll. Aber überlegen Sie es sich bitte dreimal, ob es denn wirklich sein muß. Ist es unbedingt notwendig, das Laden von Daten auf Diskette mit einer eigenen Routine zu bewerkstelligen? Muß selbst Ihre eigene Textverarbeitung ein reiner Hardwarehack sein? Schließlich soll doch der Amiga möglichst bleiben, was er ist: Ein Multitasking-Rechner. Natürlich gibt es auch genügend Situationen, bei denen es nicht vermeidbar ist. Komplexe Spiele unter dem Betriebssystem sind kaum möglich. Aber selbst dann muß nicht gleich das ganze System gekillt werden. Ein Programm muß es mindestens ermöglichen, nach Verlassen des Programms dort zu landen, wo es vor dem Start war (meistens CLI oder Workbench). Die Entschuldigung mit der "Speicherknappheit" ist out: 98% aller Amigabesitzer haben mindestens 1 MB Speicher. Die restlichen 2% werden sicher bald nachziehen, da Speichererweitungen inzwischen sehr preiswert sind. Wie dem auch sei: Ich wünsche Ihnen viel Erfolg bei Ihrer Assemblerprogrammierung. Lassen Sie sich auch bei Mißerfolgen nicht unterkriegen! Ausdauer und gute Nerven braucht jeder Programmierer, nicht nur bei Assembler. Wenn Sie mich jetzt noch fragen, welcher Assembler denn nun der beste ist, hier eine Liste der populärsten Assemblerpakete mit Vor- und Nachteilen ohne Anspruch auf Vollständigkeit. Es werden nur Includefähige Assembler berücksichtigt. Programm: ---- A68k ---- Vorteile: Hält sich an Standard. Public Domain. Zugehöriger Sourcecode(!) dabei! Gilt als "Maßstab", so erzeugt z.B. Aztec C ein A68k-Kompatibles Listing. Nachteil: Keine komplette Entwicklungumgebung, Optimizer mit Bugs, nur 68000 Code (zumindest die mir bekannte Version...) ------- ASM One ------- Vorteile: Komplette Entwicklungsumgebung, mit schnellem Editor, Monitor, und leistungsfähigem Source-Debugger. Nachteile: Nur 68000-Code, nicht ganz bugfrei, zu viele Macken des veralteten Sekas übernommen. ------ O.M.A. ------ Vorteile: Unterstützt FPU, MMU, 68020/30/40, komplette Entwicklungsumgebung, sehr guter Optimizer, leistungsfähiger Debugger. Schnell! Nachteile: Trotz seiner Qualitäten ist der Debugger sehr umständlich zu bedienen. -------- MaxonASM -------- Vorteile: Sehr gute (komplette) Entwicklungsumgebung, integrierter Reassembler, Code bis hin zum 68030, FPU und MMU. Nachteile: Relativ viele Bugs, besonders im Reassembler. Dieser beherrscht außerdem nur die Librarys bis OS 1.3 und nur 68000-Code... ------- DevPak: ------- Hier gilt in etwa das gleiche wie bei O.M.A. KAPITEL 1 Die höheren Prozessoren Spätestens seit dem Amiga 1200 muß der Programmierer einsehen, daß es nicht nur den 68000 gibt. Natürlich ist die Umstellung nicht so tragisch: Bis auf klitzekleine Ausnahmen laufen (natürlich) 68000-Programme auch auf den höheren Brüdern. Aber sie sind wesentlich schneller, besitzen erheblich mehr Adressierungsarten und auch eine ganze Reihe zusätzlicher Befehle. Die Vorteile: Sowohl Adress- als auch Datenbus sind voll auf 32 Bit ausgelegt. Um dieses Plus auch nutzen zu können, brauchen wir spezielles RAM, das auch 32Bit-breit ansprechbar ist. Man sagt: Der Prozessor braucht 32Bit-RAM. Ansonsten muß der 68020/30/40 seine Daten durch den 16Bit-Bus quetschen und ein Großteil des Geschwindigkeitsgewinns ist dahin. Doch das alles ist nicht die einzige Verbesserung: Der 68020 besitzt eine dreistufige Prefetchtechnik. Wir erinnern uns: Wenn der 68000 einen Befehl bearbeitet, hat er den nächsten Code schon eingelesen. Der 68020 geht noch weiter, bei ihm sind es deren zwei. Weitere Pluspunkte: Der Prozessor besitzt ein eigenes internes RAM von 256 Bytes, den sogenannten Cachespeicher. Das reicht für eine ganze Menge Befehle. Wenn es gelingt, eine Unterroutine auf 256 Bytes Größe zu beschränken, so kann es intern im Prozessor laufen, ohne daß er auf den Speicher zugreifen muß. Die Adressierungszyklen spart er somit ein und beschleunigt wesentlich den Programmablauf. Beim 68030 kommt nochmal ein 256 Byte-Cache speziell für Daten hinzu. Dieser Datencache hat einen sogenannten "Write-Through"-Effekt, d.h. beim Schreiben wird sofort wieder in das "richtige" RAM geschrieben, während er lesend brav im Cache bleiben kann. Der 68030 ist bei gleicher Taktfrequenz 50-70% schneller als der 68020. Beim 68040 gibt es ebenfalls diese beiden Caches, jedoch jeweils satte 4 KB!!! Zusätzlich gibt es beim Datencache den Copyback-Modus, der den Write-Through-Effekt nur zuläßt, wenn man es speziell befiehlt. Das macht den Prozessor noch schneller. Allerdings ist dieser Copyback-Modus schuld daran, daß so viele Programme mit dem 68040 nicht laufen. Der 68040 ist bei gleichem Takt viermal schneller als der 68030. Weitere Pluspunkte: Während der 68000 noch mindestens 4 Taktzyklen für einen Buszugriff benötigt, sind es beim 68020 nur noch 3, beim 68030 nur noch 2 und beim 68040 gar nur noch einer! Die Prozessoren sind in der Regel auch höher taktbar als der 68000. Der "kleinste" 68020 ist für 16 Mhz ausgelegt, das gleiche gilt für den 68030. Beim 40er ist der Minimalwert 25 Mhz, wobei dieser Prozessor intern den Takt sogar noch verdoppelt! Ein 25 Mhz-68040 läuft also eigentlich mit 50 Mhz, zumindest bei seiner internen Arbeit. Sowohl der Befehls- als auch der Registersatz ist erheblich erweitert worden. Außerdem besitzen diese CPUS mehr als dreimal so viel Adressierungsarten. Die revolutionärste Neuerung ist übrigens beim 68040 der Befehl "move16". Beispiel: move16 (a0)+,(a1)+ Kopiert 16 Bytes mit einem einzigen Befehl. Bei waitstate-freiem RAM und funktionierendem Burstmodus benötigt dieser Befehl lediglich 5 Taktzyklen! Ebenfalls wichtig ist die Coprozessor-Schnittstelle, die eine einfache Benutzung des Mathematik-Coprozessors 68881 und 68882 ermöglicht. Beim 68040 ist dieser Koprozessor schon integriert, allerdings mit einem abgespeckten Befehlssatz. Auch eine MMU gibt es, wobei ab dem 68030 bereits eine solche integriert ist. Die MMU ermöglicht uns, bestimmte physikalische Adressen in andere logische Adressen umzuwandeln. Beispiel: Wir könnten eine andere Kickstart an die Adresse $200000 laden. Die MMU sorgt nun dafür, daß der 68020/30/40 "glaubt", diese Kickstart läge normal bei $f80000. Zusätzliche Register Der 68020/30/40 besitzt einige Register mehr. Ab dem 68020 gibt es z.B. zwei Userstackpointer statt einem, und zusätzlich ein neuer Supervisorstack, speziell für Interrupts, dem sogenannten Masterstack. Erweitert wurde auch das Statusregister, das nun folgende Belegung hat: Bit: Funktion: 15 Tracebit 1 ; 00 Kein Trace, 01 Trace nur bei Sprüngen 14 Tracebit 0 ; 10 Trace nach jedem Befehl 13 Supervisorbit 12 Masterstack einschalten 11 unbenutzt 10 IRQ ; Diese drei Bits sind für die IRQ-Maske 9 IRQ 8 IRQ 7 unbenutzt 6 unbenutzt 5 unbenutzt 4 X 3 N 2 Z 1 V 0 C Seit dem 68010 gibt es das sogenannte Vektorbasis-Register. Während die Vektoren beim 68000 fix bei Adresse 0 beginnen, sind sie ab dem 68010 verschiebbar. Dieses 32Bit-Register gibt die Startadresse dieser Vektoren an. Spezielle Functionscoderegister steuern den Cache, etc.... Hier nun eine Tabelle aller Adressierungsarten (bd,Ax,Xx) Das kennen wir eigentlich schon von bd(Ax,Xx). Der Unterschied: Der konstante Offset (bd) darf hier eine Größe von 32 Bit haben oder gar weggelassen werden. (bd,PC,Xx) Dies ist dasselbe, jedoch PC-Relativ. ([bd,Ax],Xx,Faktor) Eigentlich wie oben, jedoch kann noch mit einem Faktor zusätzlich multipliert werden, der 0, 1, 2, 4 oder 8 sein darf. ([bd,Ax,Xx],Faktor) Sieht auf den ersten Blick genauso aus. Achten Sie aber auf die Klammerung! Die Indirekte Adressierung erfolgt hier erst NACH der Adressierung, beim vorherigen jedoch VORHER! ([bd,PC],Xx,Faktor) (bd,PC,Xx],Faktor) Hier wieder die gleichen nochmal, jedoch PC-Relativ. Wenn Sie bei den PC-Relativen Addressierungen statt PC ZPC schreiben, wird das Addieren des Programmcounters unterdrückt. Na ja, wers braucht.... Andere Adressierungsarten betreffen speziell die Sonderregister. Die Beschreibung dieser würde aber völlig aus dem Rahmen fallen und selbst ein ganzes Buch benötigen! Erweiterte Befehle Die Befehle zur Multiplikation und Division wurden erweitert. Jetzt können auch zwei Langworte multipliert werden. Das Ergebnis wird dann eben in zwei Register nach freier Wahl gelegt. Beispiel: mulu.l d0,d1,d4 Es wird d0 und d1 multipliziert und das Ergebnis landet in d1 und d4. So ist eine neue Größe entstanden: Das Quadword (64 Bit). Entsprechendes gilt umgekehrt für die Division. Die Condition-Sprungbefehle (BEQ etc.) können nun auch durch den gesamten Adressraum springen. Es gibt nun also gegenüber JMP keine Einschränkung mehr und somit drei verschiedene Kürzen: .B, .W, und .L. Systemsteuerung MOVEC (privilegiert) Ab dem 68010 ist die Vektorbasisadresse - wie bereits erwähnt - frei verschiebbar. Beim 68000 beginnt diese Adresse fest bei Null, beispielsweise wird der Interruptvektor Ebene 3 in $6c bestimmt. Diese Tabelle nun läßt sich im gesamten Adressraum verschieben. Hierfür dient das Steuerregister VBR. Deshalb hat Commodore auch immer wieder davor gewarnt, die Interruptvektoren direkt zu verbiegen anstatt über das Betriebssystem! Um nun die Vektorbasis nach $100000 zu verlegen, genügt der Befehl move.l #$100000,d0 movec d0,VBR Genausogut kann auch ein Adressregister herangezogen werden. Außerdem lassen sich mit MOVEC auch noch die Register SFC (Source Function Code Register) und DFC (Destination Function Code Register) beschreiben. Beim 68020 sind die Cache-Controll-Register CACR und CAAR hinzugekommen. Die Caches sollten Sie aber ab OS 2.0 über die exec.library manipulieren! MOVES Syntax: MOVES ,Rn MOVES Rn, Mit MOVES (ebenso privilegiert) werden Daten so im Speicher gelesen und geschrieben, als ab eine andere Art von Adressraum vorliegen würde. Würde beispielsweise im Supervisormodus der Befehl "MOVE $1000,d0" einer MMU über die FC-Leitung signalisieren, daß auf Supervisordaten zugegriffen wurde, so arbeitet MOVES anders: Enthält das SFC den Wert 2, und wird mit MOVES $1000,d0 gelesen, so wird ein Zugriff auf ein Usermode-Programm vorgetäuscht. Bei Lesezugriffen mit MOVES wird der Inhalt von SFC auf die FC-Leitung gelegtm bei Schreibzugriffen entsprechend DFC. SFC und DFC können folgende Werte enthalten: 1 = Usermode-Daten 2 = Usermode-Programm 5 = Supervisor-Daten 6 = Supervisor-Programm 7 = CPU (z.B. Coprozessorinterface) Coprozessoren Dieses Kapitel soll Sie ein wenig in die Programmierung des 68881/68882 einführen. Mit einem mathematischen Coprozessor steigern sich die Möglichkeiten eines Programms erheblich. Zum einen unterstützt dieser Coprozessor Fließkommaarithmetik, zum anderen sind sogar komplexe Befehle vorhanden, um zum Beispiel einen Sinus zu errechnen. Dies geht wesentlich schneller vonstatten, als wenn man diese Berechnungen softwaremäßig emulieren müßte. Die beiden Mathekünstler sind identisch. Der 68882 arbeitet aber nochmals 50% schneller. So benötigt der Befehl FASIN statt 564 Zyklen nur noch etwas über 400. Diese Zyklenzahl erscheint auf den ersten Blick zwar erschreckend hoch, aber die Programmierung "von Hand" mit der Haupt-CPU würde wesentlich länger benötigen. Ein Programm, das viel mit Fließkommaarithmetik arbeitet, läßt sich so locker 20fach beschleunigen. Der 68881/82 besitzt acht 80Bit-Fließkommaregister mit den Namen FP0 bis FP7, ein 32Bit-Statusregister, ein 16Bit-Kontrollregister sowie eine Art Programmcounter. Wie ist ein FP-Register nun aufgebaut? Das hängt von der gewählten Genauigkeit ab. Es gibt einfache Genauigkeit (SINGLE), doppelte Genauigkeit (DOUBLE) und eine erweiterte (DOUBLE EXTENDED) Genauigkeit. Hier nun der Aufbau (Anzahl Bits): Vorzeichen Exponent Mantisse Gesamt Einfach: 1 8 23 32 Doppelt: 1 11 52 64 Erweitert: 1 15 64 80 Die Grundrechenarten Hier stehen folgende Befehle zur Verfügung: FADD entspricht einer Addition FSUB ist die Subtraktion FCOM vergleicht zwei Operanden (entspricht CMP) FDIV Division FMUL Multiplikation FMOD Ermittelt Divisionsrest FREM dto, nach IEEE Standard Die Assemblersyntax ist ansonsten wie gewohnt, z.B. FADD.E (a0),FP2 Die Syntax "E," schreit nach Erklärung: .B Wie Hauptprozessor .W " " .L " " .S Einfache Genauigkeit .D Doppelte Genauigkeit .E Erweiterte Genauigkeit .P gepackte Dezimalzahl Höhere Mathematik Besonders für Vektorgrafik und ähnliche Gemeinheiten bietet sich natürlich die Benutzung dieser Superbefehle an: FCOS,FSIN,FTAN,FCOSH,FSINH,FTANH stehen für die entsprechenden trigonometrischen Funktionen. Zusätzlich existiert noch FCOSTAN zur Berechnung von Sinus und Cosinus gleichzeitig. Auch die passenden Umkehrfunktionen wie z.B. FACOS sind vorhanden. Aber auch vor Logarithmen macht dieser Rechengigant nicht halt: FETOX berechnet die Funktion e hoch x FETOXM1 wie vor, jedoch zusätzlich noch -1 FLOG10 Logarithmus zur Basis 10 FLOG2 Logarithmus zur Basis 2 FLOGn läßt Sie die Basis gar frei wählen! FLOGnp1 Ermittelt Logarithmus zur Basis e hoch x+1 FTENTOX Umkehrung von FLOG10 FTWOTOX Umkehrung von FLOG2 FSQR Quadratwurzel Hier nun ein Kernprogramm eines ganz bestimmten und sehr berühmten Algorithmus. Das Programm ist so noch nicht lauffähig, es fehlt beispielsweise "SetPixel". Es dient ja auch nur zur Demonstration der FPU-Befehle. MC68882 moveq #50,d0 ; ZYKLEN Z moveq #-2,d1 ; X1 moveq #2,d2 ; X2 moveq #-2,d3 ; Y1 moveq #2,d4 ; Y2 fmove d2,fp0 ; XD=(X2-X1)/800 fsub d1,fp0 fdiv #800,fp0 ; fp0 wird zu XD fmove d1,fp1 ; X = x1-xd fsub fp0,fp1 ; fp1 wird zu X fmove d4,fp2 ; YD=(Y2-Y1)/600 fsub d3,fp2 fdiv #600,fp2 ; fp2 wird zu YD moveq #0,d7 ; Startwert Schleife for i=0 to 799 fori: fmove d3,fp3 ; Y=Y1-YD fsub fp2,fp3 ; fp3 wird zu Y fadd fp0,fp1 ; X=X+XD moveq #0,d6 ; Startwert Schleife for j=0 to 599 forj: fadd fp2,fp3 ; Y=Y+YD fmove.x #0.5,fp4 ; FP4 ist jetzt A fmove.x #0.5,fp5 ; FP5 ist jetzt H fmove #0,fp6 ; FP6 ist jetzt B moveq #1,d5 ; Startwert Schleife for n=1 to z forn: fmove fp4,fp7 fmul fp7,fp7 fmove fp3,-(a7) ; Y retten fmove fp6,fp3 fmul fp3,fp3 fsub fp3,fp7 fsub fp1,fp7 ; M=A*A-B*B-X fmove (a7)+,fp3 ; Y restaurieren fmul fp4,fp6 ; B=A*B fadd fp6,fp6 fsub fp3,fp6 ; B=B+B-Y fmove fp7,fp4 ; A=M fmove fp3,-(a7) ; Y retten fmove fp2,-(a7) ; YD retten fabs fp4,fp2 ; ABS (A) fabs fp6,fp3 ; ABS (b) fadd fp2,fp3 ; Addieren move.l d4,-(a7) fmove fp3,d4 cmp #100,d4 bhi.B ist_groesser nextn: addq.w #1,d5 cmp.w d5,d0 bhi.W forn bra.B nextj ist_groesser: move.l (a7)+,d4 fmove (a7)+,fp2 ; YD restaurieren fmove (a7)+,fp3 ; Y restaurieren btst #0,d5 beq.B nextj jsr setpixel(pc) ; Pixel setzen nextj: addq.w #1,d6 cmp.w #512,d6 bne.W forj nexti: addq.w #1,d7 cmp.w #640,d7 bne.W fori *********************************************** Na, haben Sie gemerkt, um was es geht? Hier als Bonus gleich noch ein Beispiel: * x^2+y^2=radius * y=sqrt(radius-x^2) * mc68882 radius = 100 xmittel = 400 ymittel = 300 move.l #radius,d2 ; d2 ist konstanter radius mulu.w d2,d2 ; radius im quadrat move.l #radius,d4 ; d4 ist zählregister (max. radius) move.l d4,d0 ; d0 ist laufende x-koordinate loop: move.l d2,d1 mulu.w d0,d0 ; x^2 sub.l d0,d1 ; radius^2-x^2 fmove d1,fp1 fsqrt fp1,fp0 ; sqrt(radius^2-x^2) fmove fp0,d3 ; ykoordinate zurück in ein datenregister add.w #ymittel,d3 ; reale ykoordinate in d3 move.w d4,d5 add.w #xmittel,d5 ; reale xkoordinate in d5 subq.w #1,d4 ; zähler um eins vermindern bne.b loop ; zurück zum loop *------------------------------------------------------------------ * Und nochmal was zum Thema Kreise.. *------------------------------------------------------------------ mc68882 start: OPEN_INT ; Intuition öffnen OPEN_GFX ; GFX öffnen ; Bitte anpassen, dies ist nur eine ; Abkürzung, um Platz zu sparen! lea neuscreen(pc),a0 move.l intbase(pc),a6 jsr _LVOOpenScreen(a6) move.l d0,screenhandle move.l d0,a1 lea 84(a1),a1 ; RASTPORT move.w xmittel,d0 move.w ymittel,d1 move.w #100,yradius move.w #321,xradius Radiusloop: sub.w #20,yradius sub.w #20,xradius bmi mausi move.l gfxbase(pc),a6 bsr.B ellipse jmp radiusloop mausi: btst #6,$bfe001 bne.B mausi move.l intbase(pc),a6 move.l screenhandle(pc),a0 jsr _LVOCloseScreen(a6) CLOSE_INT ; INT & GFX schließen CLOSE_GFX rts ellipse: move.w d0,d3 move.w yradius,d4 mulu.w d4,d4 ; yradius2(d4)=yradius zum quadrat move.w xradius,d5 ; d5=xradius add.w d5,d0 ; d0=xmittel+xradius jsr _LVOMove(a6) move.w d3,d0 move.w d5,d3 ; d3=xradius mulu.w d5,d5 ; xradius2(d5)=xradius zum quadrat fmove.l d4,fp0 ; fmove.l d5,fp1 ; fp1=xradius2 fdiv.x fp1,fp0 ; Faktor(fp0)=yradius2/xradius2 fmove fp0,fp4 xloop: move.w d3,d2 ; d2=Laufvariable x mulu.w d2,d2 ; d2=x*x fmove.l d2,fp2 ; fp2=x*x fmul.x fp4,fp2 ; fp2=Faktor*x*x fmove.l d4,fp0 ; fp0=yradius2 fsub.x fp2,fp0 ; fp0=yradius2-(Faktor*x*x) fmove.x fp0,fp3 ; fp3=yradius2-(Faktor*x*x) fsqrt fp3 ; fp3=sqrt(yradius2-(Faktor*x*)) fadd.x #5.0E-01,fp3 ; rundungsfehler ausgleichen move.l d0,d5 ; d0 in d5 retten move.l d1,d6 ; d1 in d6 retten add.w d3,d0 ; offset in d0 zu x wert in d3 fmove.l fp3,d7 ; y wert in d7 add.w d7,d1 ; offset in d1 zu y wert in d7 jsr _LVODraw(a6) ; Pixel d0,d1 setzen move.l d5,d0 ; d0 wieder mit offset füllen move.l d6,d1 dbf d3,xloop ; x=x-1, Sprung zu xloop ende: rts xradius: dc.w 300 yradius: dc.w 200 xmittel: dc.w 320 ymittel: dc.w 256 screenhandle: dc.l 0 neuscreen: dc.w 0 dc.w 0 dc.w 640 dc.w 512 dc.w 1 dc.b 0 dc.b 1 dc.w $8004 dc.w 15 dc.l 0 dc.l titel dc.l 0 dc.l 0 titel: dc.b "Ellipse",0 cnop 0,4 KAPITEL 2 Grundlagen für optimiertes Programmieren Maschinencode ist rasend schnell, das steht außer Frage. Diese Geschwindigkeit hat aber auch seine Nachteile. Besonders Einsteiger verlassen sich allzusehr auf das "Wunder Assembler". Das geht so weit, daß meiner Schätzung nach 50% aller Assemblerprogrammierer schlechteren Code zustandebringen als der aktuellste C-Compiler. Schauen Sie sich mal ein C-Compilat des SAS C in seiner aktuellsten Version an! Sie können sicher gehen, daß größtenteils nichts mehr optimiert werden kann. Im Prinzip verständlich: Wie die Entwickler des SAS C schon richtig bemerkten, kann eine neu entdeckte Optimiermöglichkeit in den Compiler integriert und dann schlicht und einfach vergessen werden. Da der Mensch selbst naturgemäß eben nur ein Mensch ist, übersieht er schon mal einiges. a) DER TURBOSHIFTER Der beste Lerneffekt ergibt sich mit Hilfe von Beispielen. Deshalb wollen wir uns auch nicht lange lumpen lassen und stellen uns vor folgende Situation: Sie möchten im FastRAM, aus welchen Grund auch immer, einen bestimmten Bereich bitweise verschieben. Sagen wir, wir möchten den Inhalt 100 aneinanderliegende Worte um eine bestimmte Anzahl nach rechts verschieben. Jedem Assemblerprogrammierer wird dabei bestimmt das Kommando ROXR einfallen, mit dem mit Hilfe des X-Flags über mehrere Bereiche gerollt werden kann. Da dies nur wortweise geschehen kann, würde eine "Shiftorgie" über 100 Worte vielleicht folgendermaßen aussehen: Ich habe übrigens bewußt möglichst "miese" Befehle ausgesucht. Denn hinterher wollen wir diese Routine kräftig optimieren.... In unserem Beispiel shiften wir den Bereich um 13 Bits nach rechts. move.l #99,d0 ; Für 100 Shifts.... move.l #12,d1 ; Für 13 Shifts..... movea.l #Rollbereich,a0 ; Adresse des Bereiches (willkürlich) move.l #0,d2 roxr.l #1,d2 ; X-Flag löschen schleife: roxr.w (a0)+ sub.l #1,d0 bpl schleife move.l #Rollbereich,a0 move.l #99,d0 sub.l #1,d1 bpl schleife Wenn Sie jetzt sagen, dieses kleine Programm ist miserabel erstellt, so haben Sie vollkommen recht! Es gibt fast nichts, was nicht optimiert werden könnte. Damit es Ihnen aber nicht all zu schlecht wird, verrate ich Ihnen NICHT, wieviel Taktzyklen dieses Monstrum verschlingt. Beginnen wir zunächst mit den kleinen Optimiermöglichkeiten: Wenn wir ein Datenregister komplett verschwenden dürfen und der zu movende Bereich zwischen -128 und +127 liegt, so können wir die ersten beiden Befehle folgendermaßen formulieren: moveq #99,d0 moveq #12,d1 Der Nachteil von MOVEQ ist, daß der Bereich auf vorzeichenbehaftete Bytegröße begrenzt ist und außerdem der gemovte Wert auf Langwortgröße erweitert wird. Schreiben wir beispielsweise MOVEQ #-1,d0, so enthält hinterher das Register D0 den Wert $FFFFFFFF. Wenn das nicht ins Gewicht fällt, sollten Sie es anwenden, da es um ein Vielfaches schneller und kürzer ist. So ganz nebenbei erwähnt löscht der Befehl MOVEQ #0,D0 das Datenregister schneller (4 Zyklen 68000) als CLR.L D0 ( 6 Zyklen 68000). Auch der nächste Befehl läßt sich verbessern, nämlich durch: lea Rollbereich,a0 Ist der Datenbereich nicht weiter als 32767 Bytes entfernt, geht es noch besser: lea Rollbereich(pc),a0 Den nächsten Befehl optimieren wir wie schon angedeutet mit einem MOVEQ #0,D2 . Da der MOVE-Befehl das X-Flag nicht beeinflußt, müssen wir es künstlich löschen, wozu der nachfolgende roxr dient. Aber ein roxr.w tuts auch... Die Sequenzen sub.l #1,Datenregister und bpl können wir leicht mit der DBF-Schleife ersetzen: dbf d0,schleife Sowie: dbf d1,schleife Nach dem Ende der ersten Schleife hatten wir das Adressregister wieder neu auf den Anfangswert gesetzt, was unnötige Zeit verbrät. Eine vorherige Rettung in ein anderes Register wirkt da wahre Wunder. Sollte wirklich kein Register mehr frei sein, so ist wenigstens die Rettung auf den Stack noch eine Alternative. Hier also noch einmal die gleiche Routine (mit dem gleichen Algorithmus), jedoch wesentlich effizienter: moveq #99,d0 ; Für 100 Shifts.... moveq #12,d1 ; Für 13 Shifts..... lea Rollbereich(pc),a0 ; Adresse des Bereiches (willkürlich) moveq #0,d2 roxr.l #1,d2 ; X-Flag löschen move.l a0,a1 ; a0 retten move.l d0,d7 ; d0 retten schleife: roxr.w (a0)+ dbf d0,schleife move.l a1,a0 move.l d7,d0 dbf d1,schleife Diese Routine ist schon viel besser. Codemäßig ist sie schon sehr akzeptabel, wenn auch immer noch nicht das optimale. Ihre Trainingsaufgabe: Verbessern Sie die Routine mit dem gleichen Algorithmus noch weiter! DER ALGORITHMUS IST ALLES..... Auch das ist eine Schwäche vieler Programmierer: Es wird sich zwar vielleicht bemüht, den Code zu optimieren, nicht aber den verwendeten Algorithmus. Sie wissen sicher, daß besonders Zugriffe auf den Speicher besonders zeitintensiv sind. Die in unserem Beispiel verwendete Routine macht insgesamt satte 1300 !!! Speicherzugriffe. Da kann es einem fast schon schwindlig werden. Stellen Sie sich mal vor, Sie müßten einige Kilobytes an Daten auf diese Weise shiften. Da muß sich selbst der Prozessor veräppelt vorkommen. Doch wetten, daß wir es schaffen, unser Shiftbeispiel um den Faktor 100 (!!) zu beschleunigen? Setzen wir doch einfach mal unser logisch arbeitendes Gehirn ein. Wir stellen fest: Der Schwachpunkt der Routine ist der ROXR-Befehl, weil er in unserem Beispiel leider immer nur um ein Bit verschieben kann. Da wir aber 13 mal shiften wollen, explodieren unsere Zugriffe auf den Speicher. Schneller wäre der Zugriff, wenn die Daten in Registern wären. Da wir aber schlecht sämtliche 100 Worte in Prozessorregister legen können, brauchen wir einen anderen Geistesblitz. Die logische Schlußfolgerung: Es muß eine Möglichkeit gefunden werden, möglichst selten auf den Speicher zugreifen zu müssen. Dazu müßten wir die 13 Shifts in einem Arbeitsgang erledigen. Dummerweise gibt es kein ROXR #13, aber diesen Befehl können wir - jetzt verrate ich es endlich - simulieren! Überlegung 1: Die einzige Möglichkeit, 13 Shifts nach rechts auf einen Schlag zu machen, wäre lsr innerhalb eines Datenregisters. Der einzige Wermutstropfen: lsr darf in manchen Situationen (zum Glück nicht in allen) nur maximal 8 Shifts machen. Deshalb muß in einem solchen Fall die Aufgabe geteilt werden in lsr #8 und lsr #5 . Überlegung 2: Da bei lsr die herausgeschobenen Bits verschwinden, müssen sie irgendwie gerettet werden. Überlegung 3: Die herausgeschobenen Bits müssen im nachfolgenden Wort bzw. Langwort wieder hereingeschrieben werden. Doch nun will ich sie nicht länger auf die Folter spannen. Allerdings fehlt es nicht an einer weiteren Übungsaufgabe. Die hier gezeigte Variante arbeitet wortweise, ist aber an sich bereits hochoptimiert. Wenn Sie dieses Programm auf Anhieb verstehen, dann kann ich nur gratulieren. Die Beschleunigung ist beträchtlich: Es wird auf jedes Wort nur noch zweimal zugegriffen, einmal lesend und einmal schreibend. Es lohnt sich also schon ab 3 Shifts! Alles andere findet innerhalb von Registern statt. Doch von vorne: Sie sehen sicher den Datenbereich "shiftlist", den ich als "Shiftmaske" bezeichnet habe. Das hat folgende Bewandnis: Wenn ich zum Beispiel drei Shifts nach rechts mache, muß ich die unteren 3 Bits retten, und zwar vor der Shiftaktion mit einem AND. Bei drei Bits wird also ein "AND #7" nötig, da 7 die unteren 3 Bits repräsentieren. Diese 7 wäre in diesem Falle die Shiftmaske! Die so geretteten Bits werden nun in ein anderes Register kopiert und an den linken Rand geshiftet. Wie weit ist es bis zum linken Rand ? Bei 13 Shifts innerhalb eines Wortes exakt 3. (16 - Rechtsshifts) Prüfen Sie es nach! Das so gerettete und nach links an den Rand geschobene Wort merken wir uns nun einfach für den nächsten Durchlauf in unserem Beispiel in d2. Deshalb mußten wir anfangs d2 löschen, damit bei der ersten Aktion keine Geisterdaten vorhanden ist. Im nächsten Durchgang schließlich wird das so gemerkte Wort hineingeodert. Schauen wir uns das Funktionsprinzip mal bildlich an: Der Einfachheit wegen nehmen wir drei Beispielworte im Bitformat: 1010110010101111 1110100010001010 0101110101110110 0000000000000000 Wir brauchen am Ende ein "überschüssiges Wort", damit die Daten am Schluß der Kette nicht verloren gehen. Das erste Datenwort laden wir nach d0 und kopieren es nach d1. Dann wird mit der schon erwähnten Shiftmaske die zu rettenden Bits mit AND gesichert, deshalb auch die Kopie in D1. Bei 13 Shifts werden demnach die unteren 13 Bits gerettet. Ergebnis: 0000110010101111 Da wir diese Bits nun in d1 gerettet haben, können wir das Wort in d0 nun sorgenfrei 13fach nach rechts schieben. Ergebnis: 0000000000000101 Entsprechend werden unsere 13 geretteten Bits um drei nach links geschoben, sodaß d1 nun enthält: 0110010101111000 Jetzt odern wir den in d2 vorher gemerkten Wert in das Register d0. Da ganz Anfang noch kein vorher gemerkter Wert existiert und d2 deshalb noch 0 ist, bleibt das Ergebnis zunächst mal so... Jetzt ist d2 frei, um den neuen nach links geshifteten Wert , der sich ja noch in d1 befindet, zu vermerken: move.l d1,d2 JETZT schreiben wir das berechnete Wort, daß sich in d0 befindet, in das zuvor gelesene Wort, beispielsweise mit move.w d0,(a3)+ Momentan sehen unsere vier Worte also so aus: 0000000000000101 1110100010001010 0101110101110110 0000000000000000 Jetzt lesen wir das zweite Wort in das Register d0, und das ganze beginnt von vorne. Beachten Sie, das die "verlorenen" Bits sich nun an der richtigen Position im Register D2 befinden! Wir kopieren also d0 wieder nach d1 und maskieren nun das zweite Wort mit der Shiftmaske. Ergebnis: 0000100010001010 Jetzt wird in d0 wieder 13 mal geshiftet. Ergebnis diesmal: 0000000000000111 Die "verlorenen" Bits des vorherigen Wortes, die sich jetzt korrekterweise linksbündig in D2 befinden, brauchen wir nur hinzuzuodern, sodaß wir als Ergebnis haben: 0110010101111111 Hurra! Sie sehen, die verschollenen Bits sind nun an der richtige Stelle im zweiten Wort gelandet! Hier nun das Programm. Versuchen Sie, es genau zu analysieren. Beispielsweise enthält es als Optimierung die "ROR"-Alternative. Um zwei lsl-Kommandos (lsl #8 und lsl #5) zu verhindern, könnten wir in unserem Fall stattdessen ein ror #3 machen. Das ist schneller und erfüllt in unserer Situation den selben Zweck... moveq #13,d0 ; Für 13 Shifts move.w d0,d6 ; Anzahl Shifts merken moveq #16,d5 sub.w d6,d5 ; Anzahl nach links moveq #8,d3 ; zum Vergleichen...... add.w d0,d0 ; Für Wort in Tabelle lea shiftlist(pc),a5 move.w 0(a5,d0.W),d7 ; Shiftmaske moveq #0,d2 add.w d4,d4 addq.w #1,d4 ; Anzahl Worte...... weiter_shift: move.w (a3),d0 ; Datenwort holen move.w d0,d1 ; Nach d1 kopieren and.w d7,d1 ; mit der Shiftmaske anden... lsr.w d6,d0 ; Shiften...... cmp.w d3,d6 bhi.B besser_rohren lsl.w d5,d1 bra.B aktion besser_rohren: ror.w d6,d1 aktion: or.w d2,d0 move.w d1,d2 move.w d0,(a3)+ dbf d4,weiter_shift movem.l (a7)+,d0-d7/a3/a5 *-------------------------------------------------------------------------- Allgemeine Optimierungen Dieses Beispiel sollte eine Anregung dafür sein, nicht nur den Code an sich, sondern auch das verwendete Verfahren zu optimieren. Trotzdem läßt sich innerhalb des Prozessorbefehlssatzes einiges herausholen. Gut optimierbar sind beispielsweise Rechnungen mit Zweierpotenzen. Einige gesammelte Optimiermöglichkeiten... 1.) MULU Ein MULU #2,d0 ist programmtechnisch eine größe Stünde. Das wird aber wohl keiner machen. Besser wäre da schon lsl.l #1,d0, aber auch dies ist nicht das beste. Am schnellsten wäre in diesem Falle: add.l d0,d0 Auch bei einer Multiplikation mit 4 ist add.l d0,d0 add.l d0,d0 immer noch schneller als lsl.l #2,d0. Erst bei drei Verdoppelungen lohnt es sich nicht mehr. 2.) QUICK Benutzen Sie so oft wie nur möglich die Quickvarianten der verschiedenen Befehle! Das funktioniert meist auch mit Adressregistern: addq.l #8,d0 statt add.l #8,d0 Bei einem Adressregister können Sie auch mit .W arbeiten, da das Ergebnis dort auf Langwortgröße erweitert wird: addq.w #8,a0 wirkt wie addq.l #8,a0 Wenn Sie höhere Werte addieren wollen, gibt es zumindest bei Adressregistern eine interessante Alternative: Statt add.l #12000,a0 schreiben Sie lea 12000(a0),a0 Das klappt aber nur bis maximal 32767, ist aber dafür um einiges schneller! Oft sieht man folgenden Leichtsinn: lea 0(a0,d0.W),a0 move.l (a0),d7 Warum nicht gleich: move.l 0(a0,d0.W),d7 ? 3.) PC-RELATIV IST NICHT GLEICH PC-RELATIV Gewiß: Die PC-relativen Adressierungsarten sind schneller als die normalen Varianten. Eine ähnliche Technik wird aber oft vergessen, die selbst schlechten C-Compilern bekannt ist! Anstatt Variablen ständig direkt zu benutzen, bietet sich ein Basis-Register an, über das Sie zugreifen. Nehmen wir als Beispiel folgende Variablenliste: label1: dc.w 0 label2: dc.w 0 label3: dc.w 0 label4: dc.w 0 label5: dc.w 0 move.w label1(pc),d0 move.w d0,label2 Hier stört ein Nachteil des PC-Relativen: Nur die Quelle kann so behandelt werden, nicht aber das Ziel. Mit Hilfe eines Basisregisters läßt sich der negative Effekt verhindern: label1 = 0 label2 = 2 label3 = 4 label4 = 6 label5 = 8 variablen: dcb.w 10 Wenn Sie nun im Programmkopf einmal ein lea variablen(pc),a4 initialisieren, so können Sie auf diese Variablen immer über a4 zugreifen, sowohl mit der Quelle als auch beim Ziel: move.w label1(a4),d0 move.w d0,label2(a4) Dieser Fall ist natürlich nur ein Beispiel. Es ginge ja genausogut: move.w label1(a4),label2(a4) Oder noch besser: move.w (a4),label2(a4) Es soll damit nur gesagt werden, daß PC-Relativ allein nicht unbedingt ein echtes PC-Relatives Programm sein muß. 4.) VERGLEICHEN Ein weiterer Ansatz zum Optimieren bietet das Vergleichen. Ein cmp mit 0 ist nur bei Adressregistern notwendig, ansonsten ist die TST-Alternative schneller. Beispiel: tst.w d0 statt cmp.w #0,d0 5.) Sprünge Die meisten JMP und JSR-Kommandos in einem Programm lassen sich durch die Alternativen BRA und BSR ersetzen. Bei ganz kurzen Distanzen (-128 bis +127) sorgt das Kürzel .B für noch mehr Zeitersparnis. Prüfen Sie auch, ob Sie nicht auch exotische Adressierungsarten sinnvoll nutzen können, wie z.B. JMP 0(a3,d7.W) 6.) Lokale Variablen: Optimierungen für reentranten Code "Reentrant" heißt "wiedereintrittsfähig". Das bedeutet, daß ein Programm von mehreren Tasks gleichzeitig durchlaufen werden kann, ohne daß es zu Störungen kommt. Libraryfunktionen beispielsweise müssen diese Eigenschaft aufweisen. Es sind also nur lokale Variablen erlaubt, nicht aber statische Variablen. Daß man selbstmodifizierenden Code vermeiden soll, versteht sich von selbst..... Doch wie macht man ein Unterprogramm reentrant? Für lokale Variablen bietet sich der Stack an. Wenn Sie beispielsweise 100 Bytes für lokale Variablen benötigen, könnte der Kopf der Unterroutine so starten: link a5,#-100 movem.l d0-d7/a0-a4/a6,-(a7) ; nur wenn nötig... Relativ über das Register a5 mit NEGATIVEM Offset können Sie nun Ihre Variablen initialisieren und bearbeiten. Beispiel: move.w #200,-100(a5) Am Ende der Unterroutine schreiben Sie dann: movem.l (a7)+,d0-d7/a0-a4/a6 unlk a5 rts Was macht der LINK-Befehl ? Er rettet das angegebene Register auf den Stack und kopiert dann den Stapelzeiger in das angegebene Adressregister. Dann wird der Stapelzeiger um den angegebenen Wert erniedrigt. Somit können wir in unserem Beispiel mit negativem Offset 100 Bytes als Variablenspeicher nutzen, der sich eigentlich im Stack befindet. Beispiel: a5 enthält null und der Stapelzeiger (a7) $309ff0. Jetzt folgt der LINK a5,#-100. Nach dem Retten von a5 auf den Stack steht dieser nun auf $309fec. Dieser Wert wird nach a5 geschrieben und anschließend wird der Stackpointer noch um 100 verringert. Die Differenz von a5 und dem Stapelspeicher dient uns also nun als Variablenspeicher. Ein "emuliertes" link a5,#100 sähe so aus: move.l a5,-(a7) move.l (a7),a5 lea -100(a7),a7 Nach unlk a5 enthält a5 wieder 0 und der Stapelzeiger wieder $309ff0. Das emulierte UNLK: lea 100(a7),a7 move.l (a7)+,a5 Wenn Sie kein Register opfern können oder wollen, können Sie auch direkt den Stapelzeiger manipulieren. Bei 100 Bytes Bedarf beispielsweise mit movem.l d0-d7/a0-a6,-(a7) lea -100(a7),a7 Jetzt können Sie relativ zum Register a7, diesmal aber mit POSITIVEN Offsets, Ihre Variablen bearbeiten. Am Schluß der Routine schreiben Sie entsprechend: lea 100(a7),a7 movem.l (a7)+,d0-d7/a0-a6 Der Vorteil: Wesentlich schneller als die Link-Variante. Der Nachteil: Danach läßt sich nur umständlich noch was auf dem Stack retten, da ja dann der Offset verfälscht würde. Das Registerretten muß im Gegensatz zur LINK-Variante vorher, das Restaurieren nachher stattfinden. Vorsicht!! 7.) BESSERES JSR/RTS Kurz und Bündig: In fast allen Fällen kann beispielsweise ein jsr unter rts ersetzt werden durch jmp unter Aber verlieren Sie nicht die Übersicht! 8.) NIEDER MIT DEM POLLING Als Polling wird eine Endlosschleife bezeichnet. Besonders in der ach so geliebten Szene ist es beliebt, so auf die linke Maustaste zu warten: waitmouse: btst #6,$bfe001 bne.B waitmouse Oder aber es gibt die Situation, daß eine Libraryfunktion nicht reentrant ist oder sein darf, weil Sie eine Hardware benutzt (z.B. Drucker oder Blitter.) Es muß also verhindert werden, daß diese Routine mehr als einmal gleichzeitig durchlaufen wird. Manche "Spezialisten" behelfen sich so: zaehler: dc.w -1 routine: addq.w #1,zaehler beq.B benutzung_erlaubt benutzung_verboten: tst.w zaehler bpl.B benutzung_verboten addq.w #1,zaehler benutzung_erlaubt: . . . . subq.w #1,zaehler rts Diese Methode hat einen erschreckenden Nachteil: Alle anderen Tasks, auch der, welcher die betroffene Routine gerade benutzt, werden stark ausgebremst. Ideal wäre, wenn der Task, der die Routine gerade benutzen will, aber nicht darf, in den Waitstatus geht, bis er die Routine wieder benutzen darf. Selbst für diese Situation hat das Betriebssystem eine Lösung parat: Die Semaphoren. Beim Initalieren unseres Programms müssen wir nur eine leere Semaphore-Struktur bereithalten und initialisieren. Unser betroffenes Unterprogramm benutzt diese dann: meine_semaphore: blk.b SS_SIZE ; ohne Includes: blk.b 40,0 ; (Eigentlich wären weniger nötig, aber man ; weiß ja nie). move.l 4.W,a6 lea meine_semaphore(pc),a0 jsr _LVOInitSemaphore(a6) Unsere nur einmal gleichzeitig benutzbare Routine sähe dann so aus: routine: move.l 4.W,a6 lea meine_semaphore(pc),a0 jsr _LVOObtainSemaphore(a6) . . .Unsere Routine . move.l 4.W,a6 lea meine_semaphore(pc),a0 jsr _LVOReleaseSemaphore(a6) Intern passiert nun folgendes: "ObtainSemaphore" prüft, ab die Semaphore noch frei ist. Wenn, ja, wird das danach folgende Programm ausgeführt. Ist die Semaphore jedoch schon belegt, geht das Programm in den Wartezustand - gibt also seine Rechenzeit frei -, bis die Semaphore wieder frei ist. "ReleaseSemaphore" befreit die Semaphore wieder. Wird das vergessen, warten andere Tasks auf Ewigkeit: Sie halten es dann wie die verstorbene Putzfrau: Sie kehren nie wieder....... Es handelt sich also quasi um ein "bedingtes Wait". Wenn wir schon dabei sind: Hier eine Routine, die ein Warten auf die linke Maustaste ohne Polling ermöglicht. Dabei sind keine Kenntnisse des Input-Device etc. nötig: move.l 4.W,a6 moveq #-1,d0 jsr _LVOAllocSignal(a6) move.w d0,sigbit moveq #0,d1 bset d0,d1 move.l d1,sigmask moveq #5,d0 lea vbiint(pc),a1 move.l #irqroutine,18(a1) jsr _LVOAddIntServer(a6) waitmouse: move.l sigmask(pc),d0 move.l 4.W,a6 jsr _LVOWait(a6) lea vbiint(pc),a1 moveq #5,d0 jsr _LVORemIntServer(a6) move.w sigbit(pc),d0 jsr _LVOFreeSignal(a6) irqroutine: mvoem.l d1-d7/a1-a6,-(a7) btst #6,$bfe001 bne.B keine_maus move.l mytask(pc),a1 ; Hier muß noch IHR Task ; eingetragen werden! move.l sigmask(pc),d0 move.l 4.W,a6 jsr _LVOSignal(a6) keine_maus: movem.l (a7)+,d1-d7/a1-a6 moveq #0,d0 lea $dff000,a0 rts mytask: dc.l 0 sigmask: dc.l 0 sigbit: dc.l 0 vbiint: dc.l 0,0 dc.b 2,5 dc.l 0,0,0 cnop 0,4 Zum Schluß dieses Kapitels noch einen Tip: Einige Assembler können Programmcode bereits automatisch optimieren. Besonders der OMA und der MaxonASM beherrschen dies prächtig. Algorithmen verbessern können sie aber (natürlich) nicht. Er: "Mein Speicher wird knapp!" Sie: "Macht nix, räumen wir den Mist eben in den Keller...." KAPITEL 3 OS 2.0 kontra OS 3.0 Das Betriebssystem OS 2.0 war gegenüber den 1er-Versionen eine erhebliche Steigerung in Punkto Vielseitigkeit, Komfort und Stabilität. Doch es wurde leider NUR auf Funktionalität, nicht jedoch auf GESCHWINDIGKEIT Wert gelegt, von einigen Ausnahmen abgesehen. (Das Diskettenhandling ist z.B. erheblich schneller geworden). Frei von Fehlern war es auch nicht, aber welches Programm ist das schon. Bei OS 3.0 ging es einen kräftigen Schritt voran: Es wurden nochmals eine ganze Menge Funktionen hinzugefügt, nicht nur wegen den neuen AA-Chips (aber hauptsächlich). Diesmal hat sich das Entwicklerteam aber auch bemüht, das System auf Geschwindigkeit hin zu optimieren. Die meisten Grafikoperationen wurden erheblich beschleunigt, viele wirklich blödsinnige Systembremsen eliminiert. Falls Sie OS 2.0 und OS 3.0 kennen, haben Sie sich sicherlich über das wesentlich schnellere Windowhandling und Textscrolling gefreut. Eine Optimierung habe ich als Beispiel einmal genauer untersucht und stelle sie ihnen auch live vor: WritePixel() der "graphics.library" Der Programmteil, der für das Clipping zuständig ist, wurde weggelassen. Es geht mal nur um das Zeichnen des Punktes selbst. Unter OS 2.0 wird tatsächlich für das Setzen eines einzigen Pünktchens der Blitter eingesetzt: ; ; WritePixel von OS 2.0, Programmteil "Punkt setzen" ; MOVE.L $0004(A1),A0 ; Bitmap des Rastports LB_0D96 MULS.W (A0),D1 ; Bytes/Zeile * Zeile MOVE.W D0,D3 ; X-Koordinate retten EXT.L D0 ; Auf Langwort erweitern ASR.L #3,D0 ; /8 teilen ADD.L D0,D1 ; Zum Zeilenoffset addieren MOVEQ #$0F,D0 ; Bitmaske (0-15) AND.W D0,D3 ; Bitnummer SUB.W D3,D0 ; ermitteln MOVE.B $0005(A0),D3 ; Anzahl Planes LEA $0008(A0),A0 ; Start der Planeliste MOVEQ #-$01,D2 ; Für Blittermaske LEA $00DFF000,A4 ; Basis Customchips ADDQ.W #1,$00AA(A6) ; Blitter-Counter BEQ.B LB_0DC0 ; leer, also frei BSR.W $00FA0984 ; Arbeitet der Blitter noch ? LB_0DC0 TST.B $0002(A4) ; Blitter-DMA BTST #$06,$0002(A4) ; noch in Arbeit ? BEQ.B LB_0DD0 ; nein BSR.W $00F9FDF0 ; Warten auf Blitter LB_0DD0 MOVE.L D2,$0044(A4) ; Blittermaske setzen CLR.W D2 ; Unteres Wort löschen MOVE.W D2,$0042(A4) ; CON1 löschen BSET D0,D2 ; Zu setzendes Bit eintragen MOVE.L D2,$0072(A4) ; Quelldaten B und A CLR.W D2 ; wieder löschen MOVE.W #$0300,D0 ; Nur DMA A und D LB_0DE6 BTST D2,$0018(A1) ; Diese Bitplane maskiert ? BEQ.B LB_0E16 ; ja MOVE.B $28(A1,D2.W),D0 ; Minterm lesen TST.B $0002(A4) ; DMA noch frei ? BTST #$06,$0002(A4) ; BEQ.B LB_0E00 ; ja BSR.W $00F9FDF0 ; Warten auf Blitter LB_0E00 MOVE.W D0,$0040(A4) ; BLTCON0 setzen MOVE.L (A0),A5 ; Plane holen ADDA.L D1,A5 ; Offset dazu MOVE.L A5,$0048(A4) ; Quelle C MOVE.L A5,$0054(A4) ; Ziel D MOVE.W #$0041,$0058(A4) ; Blitter starten LB_0E16 ADDQ.L #4,A0 ; nächste Plane ADDQ.W #1,D2 ; CMP.B D2,D3 ; noch ein Bit ? BNE.B LB_0DE6 ; ja Allein das Warten auf den Blitter und das Initialisieren desselben braucht schon soviel Zeit, daß man vier Pixel mit dem Prozessor setzen könnte. Das hat auch Commodore erkannt. Nun wurde bei OS 3.0 die Routine "ReadPixel" quasi ins Umgekehrte gewandelt. Der Punkt wird nun mit der CPU gezeichnet. Ergebnis: Bis zu 6 mal schneller als unter OS 2.0: ; ; WritePixel von OS 3.0, Programmteil "Punkt setzen" ; LB_6CB6 MOVE.L $0004(A1),A0 ; BitMap des Rastports LB_6CBA MULS.W (A0),D1 ; Bytes pro Zeile*Zeilenstart MOVE.W D0,D3 ; X-Pos retten EXT.L D0 ; Auf Langwort bringen ASR.L #3,D0 ; /8 für Byteoffset ADD.L D0,D1 ; merken... MOVE.L D1,A5 ; Als Offset nach a5 MOVEQ #$07,D0 ; Bitmaske 0-7 AND.W D0,D3 ; Betroffene Bitnummer SUB.W D3,D0 ; ermitteln MOVE.B $0005(A0),D3 ; Anzahl Planes SUBQ.W #1,D3 ; -1 für schleife ADDQ.L #8,A0 ; Start der Planeliste MOVE.B $0018(A1),D1 ; Maske LEA $00DFF000,A4 ADDQ.W #1,$00AA(A6) ; Blitterzähler erhöhen BEQ.B LB_6CE8 ; (Muß sein, wegen fremder Blitts!) BSR.W $002275A0 ; Warten LB_6CE8 TST.B $0002(A4) ; DMA beendet ? BTST #$06,$0002(A4) BEQ.B LB_6CF8 ; ja BSR.W $00227FD4 ; Warten LB_6CF8 LEA $0028(A1),A3 ; Adresse der Minterme LB_6CFC MOVE.B (A3)+,D2 ; Minterm lesen MOVE.L (A0)+,A4 ; Erste Plane LSR.B #1,D1 ; Maskeninhalt prüfen BCC.B LB_6D28 ; Nicht zeichnen! TST.B D2 ; Minterm positiv ? BPL.B LB_6D16 ; ja ADD.B D2,D2 ; Minterm *2 BPL.B LB_6D10 ; immer noch positiv... BSET D0,$00(A4,A5.L) ; Bit setzen LB_6D10 DBF D3,LB_6CFC ; Nächste Plane BRA.B LB_6D2C ; Ende LB_6D16 ADD.B D2,D2 ; Minterm *2 BMI.B LB_6D24 ; Negativ... BCLR D0,$00(A4,A5.L) ; Bit löschen DBF D3,LB_6CFC ; Nächste Plane BRA.B LB_6D2C ; Ende LB_6D24 BCHG D0,$00(A4,A5.L) ; Bit invertieren LB_6D28 DBF D3,LB_6CFC ; Nächste Plane.... LB_6D2C Wahrhaftig ist die Routine erheblich schneller, es lohnt sich also! Da die Routine OS2.0-Kompatibel ist, könnte man praktisch diese Routine in OS2.0 reinpatchen! NEUE BITMAP-FORMATE Ganz wichtig zu wissen: Nur wer wirklich systemkonform programmiert hat, kann sichergehen, daß sein Programm auch auf OS 3.0 läuft. Das neue Kickstart-Flaggschiff straft schlechtes Programmieren noch mehr als OS 2.0! Oft wird nämlich "BytesPerRow" innerhalb der Bitmap-Struktur als die Breite der Bitplane interpretiert. Das stimmt aber schon bei Interlacebildern nicht mehr. Sehen wir uns aber mal die Struktur der Bitmap unter OS 3.0 an: ; ; BitMap ; dc.w XXXX ; bm_BytesPerRow/Bytes pro Zeile dc.w XXXX ; bm_Rows / Zeilen dc.b XX ; bm_Flags dc.b XX ; bm_Depth/ Anzahl Planes dc.w XXXX ; Noch(!) unbenutzt blk.l 8,0 ; Acht Zeiger auf Bitplanes Die Flags bei bm_Flags haben folgenden Aufbau: CLEAR Bit 0 DISPLAYABLE Bit 1 INTERLEAVED Bit 2 STANDARD Bit 3 MINPLANES Bit 4 Neu ist beispielsweise bei OS 3.0 "Interleaved". Das bedeutet, die Planes sind folgendermaßen aufgebaut: Zu erst alle Zeilen 0 ALLER Bitplanes Dann alle Zeilen 1 ALLER Bitplanes usw.... In diesem Zusammenhang ist der richtige Umgang mit "BytesPerRow" noch wichtiger wie bei OS 2.0. Die offizielle Definition lautet nicht, wie der Name eigentlich sagt, "Bytes pro Zeile", sondern: BytesPerRow ist die Anzahl Bytes, die zum aktuellen Zeiger hinzuaddiert werden muß, um exakt eine Zeile weiter an dem selben X-Offset zu landen. Bei 5 Bitplanes Lowres (320 Puntke) INTERLEAVE werden Sie also als BytesPerRow nicht 40, sondern 200 finden....... Seit OS 2.0 muß außerdem ein Bitplanepointer auch nicht unbedingt auf eine echte Bitplane zeigen. Denn: Eine 0 bedeutet, daß von einer komplett mit Nullen gefüllten Plane ausgegangen werden muß. Ein -1 ($FFFFFFFF) bedeutet, daß von einer komplett gefüllten Plane ausgegangen werden muß. Das spart in vielen Situationen sehr wertvolles ChipRAM! Für BltBitmap() (Offset -30) gilt also: a0 = Zeiger auf Quellbitmap-Struktur a1 = Zeiger auf Zielbitmap-Struktur d0 = X-Offset Quellbitmap d1 = Y-Offset Quellbitmap d2 = X-Offset Zielbitmap d3 = Y-Offset ZielBitmap d4 = Breite des Blits in Pixel d5 = Höhe des Blits in Zeilen d6 = Minterm d7 = Maske Ergebnis: Anzahl geblitteter Planes (d0) Dabei gilt wie schon gesagt: Enthält ein Bitplanezeiger eine 0, so wird das behandelt, als handelt es sich um eine leere Plane, bei -1 jedoch um eine volle Plane. Dieses Feature gibt es erst seit OS 2.0 !! Um eine BitMap "mit allen Schikanen" zu kreieren, muß man unter OS 2.0 noch folgenden Weg gehen: 1. Mit "InitBitMap" eine BitMapstruktur initialisieren 2. Mit AllocRaster die Planes anfordern und in die Struktur schreiben. Seit OS 3.0 gibts eine Abkürzung: AllocBitMap d0 Breite d1 Höhe d2 Anzahl Planes d3 Bitmap-Flags a0 "Friend_Bitmap" Dabei bedeutet die "Friend_Bitmap" ein Zeiger auf eine andere Bitmap. Das System versucht dann die Bitplane so zu konstruieren, daß ein eventuelles Blitten zu dieser "Friend_BitMap" möglichst effizient ist. Kommen wir nun zu den wichtigsten Neuerungen der "graphics.library". Leider reicht in unserem Büchlein nicht der Platz für alles, aber das wesentliche fehlt nicht! FreeBitMap a0 = BitMap Entfernt eine BitMap, die mit AllocBitMap angefordert wurde und gibt auch das Chipram der Planes wieder frei. Vorsicht: Erst prüfen, ob der Blitter nicht gerade in diese BitMap blittet! GetAPen a0 = Rastport Ergebnis: d0 = Vordergrund-Farbstift Ermittelt den Vordergrundfarbstift des Rastports. Das könnte zwar auch dadurch geschehen, daß man das entsprechnde Byte im Rastport ausliest, aber in Zukunft wird das Byte für den Vordergrundfarbstift nicht mehr reichen und ein größerer Wert wird existieren. Diese Routine liest den APen also zukunftskompatibel! GetBPen a0 = Rastport Ergebnis: d0 = Hintergrundfarbstift. Die GetAPen, jedoch für den Hintergrundfarbstift "BPen". Es gilt das entsprechend gesagte... GetDrMd a0 = Rastport Ergebnis: d0 = Zeichenmodus Ermittelt den Zeichenmodus des Rastports. Auch hier gilt das gleiche wie bei den beiden letzten Routinen. GetRGB32 a0 = Colormap d0 = Erste Farbnummer d1 = Anzahl Farben a1 = zu füllende Tabelle Diese Routine ähnelt GetRGB4, ist jedoch für die neuen Farbmodi des AA-Chipsets gedacht und besitzt auch erweiterte Features. Für jeden Farbanteil (!) ist dabei ein Langwort vorgesehen! Volles Gelb wird vom Betriebssystem also als $FFFFFFFF $FFFFFFFF $00000000 verwaltet! Nach A0 kommt der Zeiger auf die entsprechende Colormap-Structur. d0 entspricht der ersten Farbnummer, zur Zeit also 0-255 bei AA. d1 wiederum möchte die Anzahl der Farben wissen. Sie müssen Platz oder Speicher für eine Tabelle beschaffen, die von dieser Routine aufgefüllt wird. Pro Farbe müssen Sie demnach 3 Langworte freihalten. Funktioniert auch ohne AA-Chips! LoadRGB32 A0 = Viewport A1 = Farbtabelle Setzt mehrere Farben im RGB32-Format auf einmal. Die Tabelle muß dabei folgendes Format haben: 1 Wort mit der Anzahl der Farben 1 Wort mit der Nummer der ersten Farbe Pro Farbe wie gehabt drei Langworte 0 für das Ende der Liste oder: 1 Wort für eine weitere Anzahl Farben 1 Wort für eine neue Startnummer Pro Farbe wieder drei Langworte 0 für das Ende der Liste oder..... Sie können also im Prinzip das ganze Farbspektrum setzen und dazwischen sogar ein paar Farben auslassen bzw. überspringen. Wenn das kein Service ist! SetRGB32 a0 = ViewPort d0 = Farbnummer d1 = Rot d2 = Grün d3 = Blau Setzt eine einzelne Farbe im RGB32 Format. Nicht vergessen: Für Weiß beispielsweise : $FFFFFFFF,$FFFFFFFF,$FFFFFFFF SetWriteMask a0 = Rastport d0 = Maske Ergebnis: 0, wenn nicht möglich, bzw. wenn das Grafikdevice dies nicht unterstützt (zukunftskompatibel zu RTG!). Setzt eine neue Schreibmaske im Rastport. In etwa gilt wieder das bei SetAPen etc. gesagte... Dies war nur ein kleiner Auszug der neuen Routinen der "graphics.library". Sie funktionieren prinzipiell auch ohne AA-Chips. Bei den Farben genügen dann aber natürlich auch weiterhin die RGB4-Varianten. KAPITEL 4 Blitter speziell und AA-Chipset Die Grafik des Amiga ist intern nach dem Bitplane-Verfahren aufgebaut, was eigentlich eine Besonderheit des Amigas ist. Wie kam es dazu? Nun, zur Zeit der Entwicklung des Amiga war RAM-Speicher noch relativ teuer. Sie wissen sicher, daß das erste Amigamodell (die erste Serie des Amiga 1000) nur 256KB Speicher hatte. Wegen dieser Knappheit mußte eine Grafikdarstellungsart entwickelt werden, die möglichst wenig Speicher verbrauchte. Man suchte ein Verfahren, daß bei 5 Bit Farbtiefe auch wirklich nur 5 Bits pro Farbe braucht, bei 3 Bit Farbtiefe nur 3 Bits und so weiter. Die einzige Möglichkeit, dies zu realisieren, sind die Bitplanes. Wie sind diese nun aufgebaut? Eigentlich ganz einfach: Stellen Sie sich eine Grafik mit einer Auflösung von 320*256 Punkten vor. Da jeder Punkt einem Bit entspricht, benötigen wir hierfür 40*256 Bytes, das sind extakt 10 KB. Der Haken: Da jedes Bit nur den Zustand 0 und 1 annehmen kann, haben wir auch entsprechend nur zwei Farben zur Verfügung. Um nun 4 Farben darzustellen, werden nun einfach quasi zwei Grafiken zu 320*256 Punkten erzeugt: Dadurch haben wir nun zwei Bits pro Farbe zur Verfügung. Jede solche Teilgrafik wird "Bitplane" genannt. Bei den bisherigen Amigas waren maximal 6 Bitplanes möglich, was theoretisch 64 Farben ergab. Spezielle Hardwaretricks ermöglichten aber mehr: Der HAM-Modus läßt 4096 in einem Bild zu. Da nur 32 Farbregister existieren, werden 6 Bitplanes je nach Modus in zwei verschiedenen Arten interpretiert. Beim sogenannten Extra Halfbright Modus (Besonderer Halbhell-Modus) bestimmt die 6.Bitplane die Helligkeit: Während die ersten 5 Planes auf eines der 32 Farbregister zeigen, schaltet die 6.Bitplane die betroffene Farbe auf halbe Helligkeit. Beispiel: Würde die Farbe 16 als volles Rot ($FFF) dargestellt, so würde die 6.Bitplane diesen Farbton auf $777 reduzieren (es wird abgerundet!), sofern das entsprechende Bit dort gesetzt ist. Beim HAM-Modus wird es um einiges komplizierter. Schließlich "muß" es die Hardware hier schaffen, theoretisch sämtliche 4096 Farben in einem Bild unterzubringen, bei nur 6 Bitplanes. Wo ist der Haken? Der HAM-Modus arbeitet folgendermaßen: Die ersten 4 Planes dienen wie bisher als Zeiger auf ein Farbregister von 0-15, wenn das entsprechende Bit der Bitplanes 5 und 6 auf 0 ist. Wir haben in diesem Fall also 16 feste Farben. Ist bei einem Pixel jedoch das Bit in Plane 5 gesetzt, geschieht folgendes: Der Rote und Grüne Anteil des LINKEN BENACHBARTEN Pixels wird unverändert übernommen. Blau wird durch den Wert ersetzt, den die ersten vier Planes angeben. Würden diese beispielsweise zusammen den Wert 0 enthalten, so würde BLAU einzeln auf 0 gesetzt. Ist bei einem Pixel das Bit in Plane 6 gesetzt, geschieht das gleiche mit Rotanzeil. Grün und Blau werden also unverändert übernommen und der rote Anzeil modifiziert. Sind die Bits in Plane 5 UND 6 gesetzt, wird der Grünwert verändert, während Rot und Blau unverändert bleiben. Von diesem "Halten und Modifizieren" der Farben hat der Modus seinen Namen: HAM bedeutet "Hold And Modify". Übrigens: Wenn versucht wird, den ersten Pixel einer Zeile zu manipulieren, ist ja eigentlich gar kein linker benachbarter Pixel da. In diesem Falle wird die Hintergrundfarbe als "linker Pixel" interpretiert. Sie haben sicher die Einschränkung des HAM-Modus bemerkt: Wir können zwar schön viel Farben darstellen, aber um einen kompletten Farbwert zu ändern, benötigen wir 3 Pixel. Die "Farbwechselauflösung" beträgt demnach 3 Punkte. Sicher fällt auch auf, daß es gar nicht so einfach ist, ein Malprogramm für diesen Modus zu schreiben. Der HAM- und EHB Modus funktioniert leider nur im Lowres-Modus, also bei der niedrigsten Auflösung, da bei Hires nur maximal 4 Bitplanes möglich sind. AA-CHIPSET Sind Sie Besitzer eines Amiga 1200 oder Amiga 4000, haben Sie da schon erheblich mehr Glück. Die maximale Anzahl der Bitplanes beträgt hier 8, d.h. in den Normalmodi sind bis zu 256 Farben möglich. Das besondere: Die bisherigen Einschränkungen fallen weg: Egal, welche Auflösung Sie wählen, auch bei Hires und Superhires dürfen Sie bis zu 8 Bitplanes benutzen. Aber seien Sie gewarnt: 8 Bitplanes in Superhires belasten die DMA sehr stark! In diesem Falle gleich ein wichtiger Tip für Programmierer: Gewöhnen Sie sich einfach (pauschal) an, daß alle Bitplanes an einer durch 8 teilbaren Adresse beginnen!!! Nur dann ist sichergestellt, daß alle Grafikmodi einwandfrei funktionieren. Doch das AA-Chipset begnügt sich nicht nur mit mehr Bitplanes: Auch die Farb- und Spritefähigkeiten haben sich kräftig gesteigert. Die Gesamtpalette beträgt nun nicht mehr 4096, sondern über 16 Millionen Farben. Exakt ausgedrückt: Statt bisher 4 Bits pro Farbanteil stehen nun 8 Bits pro Farbanzeil zur Verfügung. Und diese 256*256*256 Kombinationen ergeben diese 16 Millionen. Das AA-Chipset bietet auch einen neuen HAM-Modus an, der statt mit den bisherigen 6 Planes mit 8 Bitplanes arbeitet. Die ersten 6 Planes dienen wieder als Zeiger auf die Stammpalette, sie hat sich also auf 64 erhöht. Statt 16 aus 4096 stehen demnach 64 aus 16 Millionen Stammfarben zur Verfügung. Dieser neue HAM-Modus wird auch HAM8 genannt. Die Planes 7 und 8 steuern wieder wie gehabt das Halten und Modifizieren der Farben. Doch halt! Wenn zur Manipulation der Farbe nur 6 Bits zur Verfügung stehen, wie soll dann auf 16 Millionen Farben zugegriffen werden? Im Prinzip einfach: Es geht eben nur in Viererschritten. Wir müssen also "geistig" die Farbe mit 4 multiplizieren, um den echten Farbwert zu erhalten. Bei optimaler Auswahl der Stammpalette lassen sich so bis zu 1280000 Farben in einem Bild darstellen, je nach Auflösung. Einleuchtend: Je höher die Auflösung, desto mehr Pixelmanipulationen sind möglich. Und HAM8 ist ja ebenfalls in allen Auflösungen erlaubt! Kommen wir nun zu den benötigten Registern (für AA). BPLCON0 $DFF100 Bit Funktion 15 Normaler Hires-Modus 14-12 Anzahl Bitplanes (7 nur bei AA-CHIPS) 11 HAM-Modus ( Bei 8 Planes = HAM8 bei AA-Chips) 10 Dualplayfield 9 Colorburst 8 Genlock-Audio 7 - 6 Superhires Ab ECS und natürlich AA 5 - 4 1 = 8 Bitplanes (14-12 dann unbenutzt) Nur AA-Chips! 3 Lightpen-Eingang 2 Interlace 1 Externes Sync (z.B. Genlock) 0 Einschalten von BPLCON3 (ab ECS) DIE FARBEN Die Farbregister des Amiga liegen ab $DFF180 und sind amigatypisch ein Wort groß. Die oberen 4 Bits sind hierbei unbenutzt und die unteren 12 Bits geben die jeweilige Farbe im RGB-Format an. Der Gag an der Sache: Auch beim AA-Chipset ist das nicht anders. Wie aber um alles in der Welt sind trotzdem 256 Farben möglich, noch dazu mit 8 Bits pro Farbanteil ? Jetzt lüften wir das Geheimnis: Die AA-Farbmodis Um in ein Farbregister, z.B. $DFF180, eine 24Bit-Farbe zu schreiben, benutzen diese Register eine Art "Bankswitching". Man schreibt also zuerst die 4 oberen Bits pro Farbanteil, schaltet dann die Bank um und schreibt nun die unteren vier Bits. Dabei muß die entsprechende Reihenfolge eingehalten werden. Welche Bank wir nun beschreiben, bestimmt das BPLCON3-Register, welches wir durch Setzen von Bit 0 im BPLCON0-Register eingeschaltet haben. Zuständig ist das Bit 9 dieses Registers: Beispiel: Farbe $FD1455 im Register $DFF180 move.w #$0000,$dff106 ; Schreiben der oberen 4 Bits move.w #$0F15,$dff180 ; Obere 4 Bits der Farbe move.w #$0200,$dff106 ; Schreiben der unteren 4 Bits move.w #$0D45,$dff180 ; Untere 4 Bits der Farbe Ist das Bit also 0, werden die "Highbits", ist es 1, die "Lowbits" geschrieben. Natürlich wird in der Regel hierfür der Copper verwendet, und nicht der Prozessor. Sie merken sicher auch, daß der Copper hier wesentlich mehr zu tun hat. Das ist auch der Grund, warum beim Ziehen von hochauflösenden AA-Screens unter Intuition der berüchtigte schwarze "Trennbalken" zwischen den Screen viel dicker ist als bei den "alten" Amigas gewohnt! Wichtig: Jedesmal, wenn die Copperliste neu startet (COPJMP), wird das Bit automatisch auf Null gesetzt! Beachten! Die zweite Frage, die offen bleibt: Wenn ich nur 32 Farbregister habe, wie soll ich dann 256 Farben setzen? Auch dies wird wieder durch Bankswitching gelöst. Sie sehen: Das war die rettende Idee, zu den alten Farbregistern kompatibel zu bleiben! Es werden 8 Bänke mit je 32 Farbregistern verwaltet, was die gewünschten 256 Farben ergibt. Auch hier ist wieder BPLCON3 (DFF106) zuständig, diesmal die Bits 12 bis 14. Eine Tabelle erklärt es am besten! Bit 14 13 12 Palette: 0 0 0 Farben 0-31 0 0 1 Farben 32-63 0 1 0 Farben 64-95 0 1 1 Farben 96-125 1 0 0 Farben 126-159 1 0 1 Farben 160-191 1 1 0 Farben 192-223 1 1 1 Farben 224-255 NEUE SPRITEMODI Sprites sind bei den AA-Chips nicht mehr auf Lores beschränkt. Bit 7 und 6 des BPLCON3-Registers (dff106) bestimmen die verwendete Auflösung: Bit 7 6 Auflösung 0 0 Lowres 1 0 Hires 0 1 Lowres 1 1 SuperHires Neue Spritegrößen: Nur Hires- und Superhires-Sprites alleine wären nicht reizvoll, schließlich sind die Sprites eh schon klein genug. Zuständig ist das Register $DFF1FC, und dort wiederum die Bits 2 und 3. Bit 3 2 Breite 0 0 16 1 0 32 0 1 32 1 1 64 Beachten Sie: Entsprechend ändert sich auch die Handhabung der Datenliste. Bei 16Pixel-Sprites bleibt alles beim Alten. Datenformat 32Pixel-Sprites: 2 Langworte als Controlldaten, statt bisher Worte Spritedaten im Langwortformat. Das erste Langwort: Das erste Wort ist wie zu erwarten das Highword. Das zweite Wort enthält das entsprechende zweite Kontrollwort. Das zweite Langwort: Das Highword entspricht dem zweiten Kontrollwort des ersten Langwortes. Das Lowword wird auf 0 gesetzt. Datenformat 64Pixel-Sprites: Diesmal sind es gleich zwei Doppel-Langworte, die als Controllwerte dienen. Danach folgen die Doppellangworte für die Daten. Das erste Doppellangwort: Erstes Kontrollwort,Zweites Kontrollwort, Zweites Kontrollwort, 0 Das zweite Doppellangwort: Zweites Kontrollwort,Erstes Kontrollwort,Erstes Kontrollwort,0 Die Spritefarben Bisher begann die Spritefarbtabelle fix ab Farbe 16. Bei den AA-Chips dagegen ist sie frei wählbar. Hierzu dienen die Bits 4-7 des Registers $DFF10C. Bei der Datenmenge hilft natürlich wieder am besten eine Tabelle: Bit 7 6 5 4 Spritefarben ab... 0 0 0 0 $DFF180 Farbbank 0 0 0 0 1 $DFF1A0 Farbbank 0 0 0 1 0 $DFF180 Farbbank 1 0 0 1 1 $DFF1A0 Farbbank 1 0 1 0 0 $DFF180 Farbbank 2 0 1 0 1 $DFF1A0 Farbbank 2 0 1 1 0 $DFF180 Farbbank 3 0 1 1 1 $DFF1A0 Farbbank 3 1 0 0 0 $DFF180 Farbbank 4 1 0 0 1 $DFF1A0 Farbbank 4 1 0 1 0 $DFF180 Farbbank 5 1 0 1 1 $DFF1A0 Farbbank 5 1 1 0 0 $DFF180 Farbbank 6 1 1 0 1 $DFF1A0 Farbbank 6 1 1 1 0 $DFF180 Farbbank 7 1 1 1 1 $DFF1A0 Farbbank 7 4.1.2.) Der Blitter Zunächst zu einer neuen Registerbelegung: Ab dem ECS-Chipsatz lassen sich Bereiche bis zu 32768*32768 blitten. Da das alte BLTSIZE-Register hierfür aber nicht mehr ausreicht, ist ein neues hinzugekommen, daß diesmal zwei Register belegt: BLTSIZEV $DFF05C Anzahl Zeilen BLTSIZEH $DFF05E Anzahl Worte/Zeile (11Bit) Exakt diese Reihenfolge muß eingehalten werden, da BLTSIZEH den Blitter startet. Doch nun zum Blitter selbst. Viele Programmierer haben erhebliche Probleme mit den sogenannten Mintermen. Diese Boolschen Gleichungen bestimmen die Blitteroperation. Sehen wir uns der Vollständigkeit halber diese Terme und das entsprechende Register nochmal an: BLTCON0 $DFF040, Bits 7 bis 0 7 6 5 4 3 2 1 0 ABC ABc AbC Abc aBC aBc abC abc Als Beispiel sei gegeben: Quelle A: Maske, welche festlegt, was geblittet werden soll. Quelle B: Die zu blittenden Daten Quelle C: Das Ziel selbst. Ergebnis ist wiederum Ziel, auch D genannt. Bei dieser Kombination bewirkt der Minterm $C0 ein einfaches Kopieren. $30 invertiert unsere Daten erst, bevor sie ins Ziel gelangen. $50 schließlich bewirkt mit der Quelle gar nichts. Lediglich das Ziel wird invertiert. (Sie vermuten richtig, daß in diesem Falle der Inhalt von Quelle B egal ist). Ich stelle Ihnen nun ein Assemblerprogramm vor, daß einen Blitt anhand dieser Minterme simuliert. Dabei enthält das Register D0 die Maske, D1 die Quelle und D2 das Ziel. Das Blittergebnis landet schließlich im Register D7. Das Mintermbyte übergeben wir in D6. Experimentieren Sie mit diesem Progrämmchen, dann werden Sie diese Terme sicher schnell verstehen: ; ; Minterm-Simulation in Assembler ; moveq #-1,d0 ; Maske move.l #$89abcdef,d1 ; Quelle als Beispiel move.l #$55555555,d2 ; Noch-Inhalt des Ziels move.b #$c0,d6 ; Minterm moveq #0,d7 ; Hier kommt das Ergebnis hin lsr.b #1,d6 ; Mintermbit 0 ? bcc.B nicht0 ; nein ; ; Minterm abc berechnen ; move.l d0,d3 move.l d1,d5 move.l d2,d6 and.l d3,d5 and.l d5,d6 not.l d6 or.l d6,d7 nicht0: lsr.b #1,d6 ; Mintermbit 1 ? bcc.B nicht1 ; Nein ; ; Minterm abC berechnen ; move.l d0,d3 move.l d1,d5 move.l d2,d6 not.l d3 not.l d5 and.l d3,d5 and.l d5,d6 or.l d6,d7 nicht1: lsr.b #1,d6 ; Mintermbit 2 ? bcc.B nicht2 ; Nein ; ; Minterm aBc berechnen ; move.l d0,d3 move.l d1,d5 move.l d2,d6 not.l d3 not.l d6 and.l d3,d5 and.l d5,d6 or.l d6,d7 nicht2: lsr.b #1,d6 ; Mintermbit 3 ? bcc.B nicht3 ; ; Minterm aBC berechnen ; move.l d0,d3 move.l d1,d5 move.l d2,d6 not.l d3 and.l d3,d5 and.l d5,d6 or.l d6,d7 nicht3: lsr.b #1,d6 ; Mintermbit 4 ? bcc.B nicht4 ; ; Minterm Abc berechnen ; move.l d0,d3 move.l d1,d5 move.l d2,d6 not.l d5 not.l d6 and.l d3,d5 and.l d5,d6 or.l d6,d7 nicht4: lsr.b #1,d6 ; Mintermbit 5 ? bcc.B nicht5 ; ; Minterm AbC berechnen ; move.l d0,d3 move.l d1,d5 move.l d2,d6 not.l d5 and.l d3,d5 and.l d5,d6 or.l d6,d7 nicht5: lsr.b #1,d6 ; Mintermbit 6 ? bcc.B nicht6 ; ; Mintermbit ABc berechnen ; move.l d0,d3 move.l d1,d5 move.l d2,d6 not.l d6 and.l d3,d5 and.l d5,d6 or.l d6,d7 nicht6: lsr.b #1,d6 ; Mintermbit 7 ? bcc.B nicht7 ; ; Mintermbit ABC berechnen ; move.l d0,d3 move.l d1,d5 move.l d2,d6 and.l d3,d5 and.l d5,d6 or.l d6,d7 nicht7: Damit haben Sie in d7 das Ergebnis, das auch der Blitter gehabt hätte. DAS LINIENZIEHEN Der Blitter benutzt zum Zeichnen einer Linie den Bresenham-Algorithmus, einer der schnellsten Verfahren der Welt. Unverständlicherweise haben viele Programmierer Probleme, dieses Prinzip zu verstehen. Deshalb zeige ich Ihnen hier einmal eine Variante mit dem Prozessor. Ich habe sie absichtlich etwas umständlich programmiert, um das Verständnis zu erhöhen. In Wirklichkeit läßt sich der Bresenham-Algorithmus noch viel kompakter und effizienter schreiben. Optimieren Sie doch mal dieses Programm! Streng genommen paßt es hier zwar nicht zum Thema Blitter, aber die Verständlichkeit des Blitter-Linien-Ziehens wird erhöht! Der Bresenham-Algorithmus vermeidet zeitraubende Orgien mit Sinus/Cosinus und ähnlichem Schnickschnack. Noch heute werden oft Linien auf lahme Weise programmiert. Eigentlich unverständlich. Dabei ist "Bresenham" so einfach: Es kommt nur mit Auf- bzw. Abwärtszählen aus. Das hier vorgestellte Programm benutzt der Einfachheit halber zum Zeichnen der jeweiligen Pixel die Routine "WritePixel" der ",graphics.library", die als Ziel nicht die Bitplanes, sondern den Rastport erwartet. Vorteil: Selbst Clipping wird berücksichtigt. Ist der Rastport also z.B. teilweise von einem anderen Rastport (z.B. Window) überdeckt, wird dort nicht hineingezeichnet, sondern in den gepufferten verdeckten Bereich. Als Bonus unterstützt die Routine sogar die Linienmuster. Aber wie gesagt: Natürlich gibt es Unmengen schnellerer Varianten. * * Linien mit Bresenham, unterstützt Linepatterns * * Code: Manfred Leidorf * Hinweis: Umständlich programmiert zwecks * besserem Verständnis * * Die Routine holt sich die Startkoordinaten aus * dem Rastport und ist fast 100% funktionskompatibel * zur Betriebssystem-Routine "Draw" * * a1: Zielrastport * d0: ZielX * d1: ZielY * * Falls Sie keine Includes haben: * _LVOOpenLibrary = -552 _LVOMove = -240 _LVOWritePixel = -324 move.l 4.W,a6 lea gfxname(pc),a1 moveq #0,d0 jsr _LVOOpenLibrary(a6) move.l d0,gfxbase movem.l d2-d7/a0-a6,-(a7) ; Register retten move.l gfxbase(pc),a6 move.w d0,d2 ; ZielX move.w d1,d3 ; ZielY move.w 36(a1),d0 ; StartX (Start X im Rastport) move.w 38(a1),d1 ; StartY (Start Y im Rastport) movem.l d0-d1/a1,-(a7) move.w d2,d0 move.w d3,d1 jsr _LVOMove(a6) ; Grafikcursor setzen movem.l (a7)+,d0-d1/a1 *-------------------------------------------------------------------------- * DeltaX und DeltaY bilden *-------------------------------------------------------------------------- move.w d3,d7 ; ZielY nach d7 move.w d2,d6 ; ZielX nach d6 sub.w d0,d6 ; DeltaX bpl.B posi1 neg.w d6 ; Eventuell positiv machen posi1: sub.w d1,d7 ; DeltaY bpl.B posi2 neg.w d7 ; Eventuell positiv machen posi2: *-------------------------------------------------------------------------- cmp.w d0,d2 ; X1 < X2 ? bgt.B oktant0167 ; ja *-------------------------------------------------------------------------- * X1 war größer als X2 *-------------------------------------------------------------------------- oktant2345: cmp.w d1,d3 ; Y1 < Y2 ? bgt.B oktant23 ; Dann Oktand 2 oder 3 oktant45: cmp.w d6,d7 ; d6: DeltaX d7: DeltaY bgt.B oktant5 bra.B oktant4 oktant23: cmp.w d6,d7 ; d6: DeltaX d7: DeltaY bgt.B oktant2 bra.B oktant3 *-------------------------------------------------------------------------- * X2 war größer als X1 *-------------------------------------------------------------------------- oktant0167: cmp.w d1,d3 bgt.B oktant01 ; Dann Oktand 0 oder 1 oktant67: cmp.w d6,d7 ; d6: DeltaX d7: DeltaY bgt.B oktant6 bra.B oktant7 oktant01: cmp.w d6,d7 ; d6: DeltaX d7: DeltaY bgt.B oktant1 bra.W oktant0 *---------------------------------------------------------------------------- * Einsprünge eintragen *---------------------------------------------------------------------------- oktant0: lea oktant0routine(pc),a3 bra.B variablen oktant1: lea oktant1routine(pc),a3 bra.B variablen oktant2: lea oktant2routine(pc),a3 bra.B variablen oktant3: lea oktant3routine(pc),a3 bra.B variablen oktant4: lea oktant4routine(pc),a3 bra.B variablen oktant5: lea oktant5routine(pc),a3 bra.B variablen oktant6: lea oktant6routine(pc),a3 bra.B variablen oktant7: lea oktant7routine(pc),a3 *---------------------------------------------------------------------------- * Sondervariablen berechnen *---------------------------------------------------------------------------- variablen: cmp.w d6,d7 bgt.B kleindelta6 ; X-Differenz ist Kleindelta move.w d7,d5 ; Kleindelta nach d5 move.w d6,d4 ; Großdelta nach d4 bra.B arithmetisch ; Zu den Berechnungen.... kleindelta6: move.w d6,d5 ; Kleindelta nach d5 move.w d7,d4 ; Großdelta nach d4 *---------------------------------------------------------------------------- * Arithmetische Berechnungen *---------------------------------------------------------------------------- arithmetisch: add.w d5,d5 ; 2 x Kdelta move.w d4,d6 ; GDelta für Schleife nach d6 move.w d4,d7 ; GDelta nochmal nach d7 add.w d4,d4 ; 2 x GDelta *---------------------------------------------------------------------------- * Jetzt kann es eigentlich losgehen........ *---------------------------------------------------------------------------- move.w 34(a1),d2 ; Linienmuster aus Rastport holen jmp (a3) ; Einspringen... nop oktant0routine: rol.w #1,d2 bcc.B muster0 movem.l d0-d1,-(a7) jsr _LVOWritePixel(a6) movem.l (a7)+,d0-d1 muster0: addq.w #1,d0 ; X+1 sub.w d5,d7 bpl.B noch_positiv addq.w #1,d1 ; Y+1 add.w d4,d7 noch_positiv: dbf d6,oktant0routine bra.W line_finito oktant1routine: rol.w d2 bcc.B muster1 movem.l d0-d1,-(a7) jsr _LVOWritePixel(a6) movem.l (a7)+,d0-d1 muster1: addq.w #1,d1 ; Y+1 sub.w d5,d7 bpl.B noch_positiv3 addq.w #1,d0 ; X+1 add.w d4,d7 noch_positiv3: dbf d6,oktant1routine bra.W line_finito oktant2routine: rol.w #1,d2 bcc.B muster2 movem.l d0-d1,-(a7) jsr _LVOWritePixel(a6) movem.l (a7)+,d0-d1 muster2: addq.w #1,d1 ; Y+1 sub.w d5,d7 bpl.B noch_positiv6 subq.w #1,d0 ; X-1 add.w d4,d7 noch_positiv6: dbf d6,oktant2routine bra.W line_finito oktant3routine: rol.w #1,d2 bcc.B muster3 movem.l d0-d1,-(a7) jsr _LVOWritePixel(a6) movem.l (a7)+,d0-d1 muster3: subq.w #1,d0 ; X-1 sub.w d5,d7 bpl.B noch_positiv8 addq.w #1,d1 ; Y+1 add.w d4,d7 noch_positiv8: dbf d6,oktant3routine bra.B line_finito oktant4routine: rol.w #1,d2 bcc.B muster4 movem.l d0-d1,-(a7) jsr _LVOWritePixel(a6) movem.l (a7)+,d0-d1 muster4: subq.w #1,d0 ; X-1 sub.w d5,d7 bpl.B noch_positiv2 subq.w #1,d1 ; Y-1 add.w d4,d7 noch_positiv2: dbf d6,oktant4routine bra.B line_finito oktant5routine: rol.w #1,d2 bcc.B muster5 movem.l d0-d1,-(a7) jsr _LVOWritePixel(a6) movem.l (a7)+,d0-d1 muster5: subq.w #1,d1 ; Y-1 sub.w d5,d7 bpl.B noch_positiv4 subq.w #1,d0 ; X-1 add.w d4,d7 noch_positiv4: dbf d6,oktant5routine bra.B line_finito oktant6routine: rol.w #1,d2 bcc.B muster6 movem.l d0-d1,-(a7) jsr _LVOWritePixel(a6) movem.l (a7)+,d0-d1 muster6: subq.w #1,d1 ; Y-1 sub.w d5,d7 bpl.B noch_positiv5 addq.w #1,d0 ; X+1 add.w d4,d7 noch_positiv5: dbf d6,oktant6routine bra.B line_finito oktant7routine: rol.w #1,d2 bcc.B muster7 movem.l d0-d1,-(a7) jsr _LVOWritePixel(a6) movem.l (a7)+,d0-d1 muster7: addq.w #1,d0 ; X+1 sub.w d5,d7 bpl.B noch_positiv7 subq.w #1,d1 ; Y-1 add.w d4,d7 noch_positiv7: dbf d6,oktant7routine ****************************************** * Hier wären wir fertig... ****************************************** line_finito: gfxname: dc.b "graphics.library",0 cnop 0,4 gfxbase: dc.l 0 Wie gehts beim Blitter ? In einem recht populären Buch zum Amiga wird bei der Beschreibung des Linemodus ein Fehler gemacht, der zu Linien führt, die einen Pixel zu kurz sind. Dieser Patzer entstand, weil der betroffene Autor die Differenz von Start und Ziel mit der Größe verwechselt hat. Um die richtige Größe zu erhalten, muß man nach der Ermittlung der Differenz noch 1 addieren. Logisch, oder ? Wie schon die Prozessorversion, arbeitet auch der Blitter mit dem Oktantensysem, da ja auch er den Bresenham-Algorithmus benutzt. Die Oktanten sind beim Blitter so aufgeteilt: 2 1 3 0 4 7 5 6 Nennen wir die Koordinaten nun StartX, StartY, ZielX und ZielY. Wie bestimmen wir nun den richtigen Oktanten? Relativ leicht (So macht es auch das obige Programm!) Gehen wir stufenweise vor: 1. Ist StartX kleiner als ZielX? Dann kommen nur die Oktanten 0,1,6,7 in Frage, ansonsten 2,3,4,5. Sind StartX und ZielX gleich, sind wir an dieser Stelle nicht schlauer, dann könnten es nämlich alle acht Oktanten sein. 2. Ist StartY kleiner als ZielY? Dann kommen nur die Oktanten 0,1,2,3 in Frage, ansonsten 4,5,6,7. Bei Gleichheit gilt obiges Gesagte. Wir folgern logisch: Sind StartX/ZielX und StartY/ZielY gleich, so liegen wir exakt in der Mitte und der Oktant ist egal. Die Linie wäre dann auch nur 1 Punkt! 3. Jetzt müssen wir ein wenig rechnen: Bilden Sie die Differenz der X-Koordinaten. Das Ergebnis nennen wir DELTAX. Machen Sie das gleiche mit den Y-Koordinaten. Das Ergebnis nennen wir DELTAY. 4. Ist DeltaX größer als DeltaY, so kommen nur die Oktanten 0,3,4,7 in Frage, andernfalls nur 1,2,5,6. Aus diesen bisherigen Erkenntnissen läßt sich der passende Oktant ermitteln. Ungerechterweise müssen wir nun noch den Oktanten einen Code zuordnen. Oktant: Code: 0 6 1 1 2 3 3 7 4 5 5 2 6 0 7 4 Der Blitter hat die Möglichkeit, Linien mit einem 16Bit-großen Muster zu zeichnen. Außerdem gibt es einen Modus der verhindert, daß in einer Zeile mehr als 1 Pixel vorkommt. Für bestimmte Fälle ist das notwendig, beispielsweise beim korrekten Flächenfüllen mit dem Blitter. Bevor wir nun zu den Registern kommen, müssen wir aber noch ein wenig rechnen! Wir haben vorhin zwei Werte berechnet, nämlich DeltaX und DeltaY. Nehmen Sie das Delta mit dem kleineren Wert, das wir nun KleinDelta nennen wollen. Das größere nennen wir GroßDelta. 1.) Merken Sie sich KleinDelta und verdoppeln Sie den Wert. 2.) Merken Sie sich auch dieses Ergebnis. Ziehen Sie nun Großdelta von diesem Ergebnis ab. Ist es negativ, merken wir uns, daß wir das SIGN-Flag auf 1 setzen müssen. 3.) Nehmen Sie wieder das verdoppelte KleinDelta und subtrahieren Sie davon ein verdoppeltes GroßDelta. a) Das Ergebnis der Rechnung bei 2.) kommt in das Register BLTAPTL. b)In BLTCPT und BLTDPT kommt die Startadresse der Linie, genauer gesagt des Wortes, in dem die Linie beginnt. c) Das Ergebnis der Rechnung bei 3.) kommt in das Register BLTAMOD. d) Das verdoppelte KleinDelta muß nach BLTBMOD e) BLTCMOD und BLTDMOD müssen mit der Breite der Grafik in Bytes gefüttert werden, in die die Linie gezeichnet wird. f) BLTADAT bekommt den Wert $8000 g) BLTBDAT bekommt das Linienmuster! h) BLTAFWM wird mit $FFFF initialisiert. BLTSIZE startet den Blitter. Die Bits 0-5 werden fix mit 2 initialisiert, während die Bits 6-15 die Länge der Linie enthalten müssen. Die Länge entspricht dem Wert "GroßDelta"+1 !! Register BLTCON0: Beim normalen Linienziehen wird der Minterm $CA eingesetzt. Die benötigten DMA-Bits sind 1011, und die oberen 4 Bits kennzeichnen den Startpunkt innerhalb des ersten Wortes. Register BLTCON1: Die ersten vier Bit sind identisch zu BLTCON 0. Bits 11-7 werden gelöscht. Bit 6 ist das SIGN-Flag. Wie Sie es behandeln müssen, habe ich bereits erwähnt. Die Bits 4-2 enthalten den vorhin besprochenen Oktantencode. Bit 1 bestimmt, ob nur 1 Pixel pro Zeile gezeichnet werden darf. Bit 0 aktiviert den Linienmodus, muß also gesetzt werden. KAPITEL 5 Wir entwickeln eine eigene Library Oft neigt man als Programmierer ganz schnell dazu, ständig Programmteile immer wieder neu zu entwickeln, die eigentlich schon x-mal eingetippt wurden. Ein guter Weg, von dieser Schwäche abzukehren, ist das Sammeln der wichtigsten und immer wieder gebrauchten Unterroutinen. Hat man jedoch gar mehrere Routinen zu einem bestimmten Thema angesammelt, so bietet sich eine Library an. So braucht in Zukunft nur noch diese Library geöffnet zu werden, und die Routinen lassen sich dann auch von mehreren Programmen gleichzeitig benutzen, ohne diese ständig neu im Speicher zu haben. Sie stellen sich das nun besonders schwierig vor? Keine Panik auf der Titanic, es ist sogar sehr einfach. An dieser Stelle sei nochmals betont, daß Sie sich die INCLUDE-Files für Assembler besorgen sollten. Viele Händler bieten diese zum günstigen Preis an, außerdem sind bei vielen kommerziellen Assemblern (z.B. MaxonASM, O.M.A., ASMOne) diese Includes dabei. Ohne diese Includes ist das Entwickeln einer eigenen Library wahrhaft schwierig. Damit es nicht allzu kompliziert wird, wollen wir zur Probe einmal eine kleine Library entwickeln. Diese soll folgende Funktionen haben: 1. Die wichtigsten Librarys sollen auf einen Schlag geöffnet und die Zeiger darauf in einem Langwort-Array eingetragen werden. 2. Die entsprechende Routine, um diese Librarys wieder zu schließen. 3. Es soll ein Requester erscheinen, der "Sind Sie sich da sicher?" abfragt. Dieser Requester soll automatisch auf dem vordersten Screen erscheinen. 4. Es soll die Größe einer Datei festgestellt werden. Als Parameter erwartet die Routine den Filenamen samt Pfad. Grundregeln einer Library 1.) Halten Sie die Register-Konventionen ein! Das heißt konkret: Die Register d2-d7 und a2-a6 müssen nach Rückkehr wieder den alten Wert besitzen. 2.) Ergebnisse sollten möglichst dem Register d0 übergeben werden, auch Zeiger! Seltene Ausnahmen sind erlaubt. 3.) Bei Übergabeparametern sollten Sie möglichst die Register d0-d1 und a0-a1 benutzen. Mehr Parameter sind aber im Notfall erlaubt. 4.) Die Routinen müssen re-entrant sein. Das bedeutet: Mehrere Programme müssen die Routinen gleichzeitig durchlaufen können, ohne sich gegenseitig dabei zu stören. Verboten sind demnach globale Variablen! 5.) War eine Aktion nicht erfolgreich, müssen alle verbrateten Speicherbereiche und Librarys wieder freigegeben werden. Außerdem wird dann 0 dem Register d0 übergeben. (0=FALSE) Die Librarybase Nennen wir unsere kleine Library doch "spezial.library". Die "Mutter" einer Library ist die Librarybase. Diese müssen wir erstellen und in der Include-Schublade speichern. In unserem Beispiel taufen wir sie "SpezialBase.i". Dabei steht das "i" für Include. ; ; SpezialBase.i ; ; Include-File für Leidorfs Spezial-Library ; IFND SPEZIAL_BASE_I SPEZIAL_BASE_I SET 1 IFND EXEC_TYPES_I INCLUDE "exec/types.i" ENDC IFND EXEC_LISTS_I INCLUDE "exec/lists.i" ENDC IFND EXEC_LIBRARIES_I INCLUDE "exec/libraries.i" ENDC STRUCTURE SpezialBase,LIB_SIZE UBYTE sb_Flags UBYTE sb_pad ULONG sb_SysLib ULONG sb_DosLib ULONG sb_IntuiLib ULONG sb_SegList LABEL SpezialBase_SIZEOF SPEZIALNAME MACRO DC.B 'spezial.library',0 ENDM ENDC ; ; ; Die hier gezeigte Librarybase ist nur die Mindestvorraussetzung. Vor dem "LABEL "SpezialBase_SIZEOF" " können Sie noch Ihre eigenen Variablen und Zeiger einsetzen, sofern das in Ihrem Falle nötig sein sollte. Die meisten Librarys haben ja viel größere Basisstrukturen als diese hier. Dabei bedeuten: UBYTE Vorzeichenloses Byte UWORD Vorzeichenloses Wort ULONG Vorzeichenloses Langwort BYTE Vorzeichenbehaftetes Byte WORD Vorzeichenbehaftetes Wort LONG Vorzeichenbehaftetes Langwort APTR Zeiger Das Kommando "LABEL SpezialBase_SIZEOF" läßt den Assembler das Ende der Struktur erkennen. Nachdem wir diese "SpezialBase.i" in der Include-Schublade gespeichert haben, sollten wir uns noch auf die Funktionsoffsets einigen. Übrigens können Sie auch noch andere eigene Strukturen Ihrer eigenen Library in der gleichen Datei definieren. Schauen Sie sich ruhig einmal einige Commodore-Strukturen an, z.B. "exec.i", oder "gfx.i". Der Lerneffekt ist gewaltig! Die Offsets -6,-12,-18, und -24 sind dem System vorbehalten. Deshalb müssen unsere Funktionen mit dem Offset -30 beginnen. Auch hier sollten wir eine Includedatei generieren, die unsere Einsprünge beinhalten. Diese taufen wir "spezial_lib.i": ; ; Spezial_lib.i Einsprünge ; _LVOOpenAllLibs EQU -30 ; Öffnen der wichtigsten Librarys _LVOCloseAllLibs EQU -36 ; Schließen derselben _LVOAutoRequestFront EQU -42 ; Autorequest auf dem vordersten Screen _LVOCheckFileSize EQU -48 ; Um Dateigröße festzustellen Nun können wir uns an unser eigentliches Programm machen. Der Programmkopf unserer eigenen Library sieht mindestens so aus: (Aber nicht die beiden eben besprochenen Dateien dazu, die haben Sie ja hoffentlich gespeichert!) Es folgt nun alles in der echten Reihenfolge, so daß Sie es mit Ihrem Assembler gleich ausprobieren können. Bedenken Sie bitte, daß bei Ihnen selbst die Dateiverzeichnisse natürlich auch einen anderen Namen haben können! ********************************************************* * * * spezial.library * * * * Assembler Profi-Tips und Tricks * * von Manfred Leidorf * ********************************************************* INCDIR INCLUDES: ; Assigned die Includes. Kann bei ; Ihnen anders heißen!! Hier ; bedeutet es, daß die Includes im ; im logischen Gerät "INCLUDES:" zu ; finden sind! INCLUDE lvo/exec.i ; Oder Gleichwertiges mit den LVOS! INCLUDE lvo/intuition.i INCLUDE lvo/dos.i INCLUDE exec/types.i INCLUDE exec/libraries.i INCLUDE exec/lists.i INCLUDE exec/alerts.i INCLUDE exec/initializers.i INCLUDE exec/resident.i INCLUDE libraries/dos.i INCLUDE intuition/intuitionbase.i INCLUDE SpezialBase.i *-------------------------------------------------------------------- Das sind so die Grund-Includes für die eigene Library. Nun geht es schon um den eigentlichen Programmcode. Eine Library sollten Sie wie folgt beginnen, damit es keinen Absturz gibt, wenn diese versehentlich als normales Programm gestartet wird: start: moveq #0,d0 rts Nun folgt die sogenannte Resistent-Struktur, die in etwa immer so aussehen muß. Die Erläuterung erfolgt in den Kommentaren: RTSTRUC: dc.w RTC_MATCHWORD ; Resistent-Kennung dc.l RTSTRUC ; Zur Kontrolle Zeiger auf Beginn dc.l EndLib ; Zeiger auf Ende der Library dc.b RTF_AUTOINIT ; Flag für automatisches Initialisieren dc.b SPEZIALLIBVERSION ; Versionsnummer dc.b NT_LIBRARY ; Kennung "Library" dc.b MYLIBPRI ; Priorität dc.l MyLibName ; Zeiger auf Libraryname dc.l LibId ; Zeiger auf Versionsstring dc.l InitLib ; Zeiger auf Library-Initstruktur MyLibName: dc.b "confusion.library",0 ; Der Name cnop 0,4 ; Adresse begradigen SPEZIALLIBVERSION = 34 ; Ihre Versionsnummer SPEZIALLIBREVISION = 1 ; Ihre Revisionsnummer MYLIBPRI = 0 ; Die Prioriät LibId: dc.b "spezial.library 34.1 (12. Mai 93) " ; Der IDSTRING dc.b "© Manfred Leidorf 1993",13,10,0 cnop 0,4 dc.w 0 Wenn Sie sich über "Kennung Library" gewundert haben: Auch Devices beispielsweise haben diesen Kopf. Deshalb muß das Betriebssystem die Möglichkeit zur Unterscheidung haben! Es folgt nun die Initialisierungsstruktur. Diese beginnt mit der Größe der Librarybase-Struktur, dem Zeiger auf die Funktionstabelle und der Datentabelle. Danach folgt noch der Zeiger der Initialisierungsroutine. Dieses Zeigerkonzept mag anfangs verwirrend wirken, hat aber den Vorteil, daß nicht mit aller Gewalt jeder Programmteil an einer bestimmten Stelle beginnen muß! InitLib: dc.l SpezialBase_SIZEOF ; Größe der LibraryBase dc.l Functions ; Zeiger auf die Funktionstabelle dc.l Datas ; Zeiger auf eventuelle Daten dc.l LibInitRoutine ; Zeiger auf Initialisierungsroutine Functions: dc.l Open dc.l Close dc.l Expunge dc.l ExtFunc dc.l OpenAllLibs ; Öffnet GFX/INT/DOS/LAYERS/ICON dc.l CloseAllLibs ; Schließt diese Libs wieder dc.l AutoRequestFront ; Unser "Frontrequester" dc.l CheckFileSize ; Filegrößenchecker dc.l -1 ; Ende Die Routinen "Open, Close, Expunge und ExtFunc" sind für das System reserviert. Trotzdem müssen Sie diese Routinen selbst schreiben, wobei es bestimmte Grundvorschriften gibt. Aber auch eigene Routinen können dort eingebaut werden. Zum Beispiel könnten Sie veranlassen, daß beim Öffnen Ihrer Library automatisch das "audio.device" geöffnet wird und so weiter.... Das grundlegende der Routine Open ist das Öffnen der Library bzw. das Zählen, wie oft die Library schon geöffnet wurde. Denn erst wenn dieser Zähler wieder auf Null ist, darf die Library aus dem System entfernt werden. Close ist das entsprechende Gegenstück. Es überwacht die Anzahl der Library-Schließungen. Expunge ist schließlich dafür zuständig, nun tatsächlich die Library aus dem System zu entfernen. ExtFunc ist noch unbenutzt und mit einer Dummy-Funktion belegt. Damit wurde noch Reserve geschaffen für eventuelle Erweiterungen. Es folgen nun noch die Daten. Dabei beginnen erst die vom System vorgeschriebenen Daten, danach folgen Ihre eigenen. Da wir in unserer Library eine Funktion haben, welche die Librarys "graphics,intuition, dos, layers und icon" öffnet, brauchen wir natürlich die Namen dieser Librarys. Datas: dc.b LN_TYPE,NT_LIBRARY dc.l LN_NAME,MyLibName dc.b LIB_FLAGS,LIBF_SUMUSED!LIBF_CHANGED dc.w LIB_VERSION,SPEZIALLIBVERSION dc.w LIB_REVISION,SPEZIALLIBREVISION dc.l LIB_IDSTRING,LibId dc.l 0 c_gfxname: dc.b "graphics.library",0 cnop 0,4 c_intname: dc.b "intuition.library",0 cnop 0,4 c_dosname: dc.b "dos.library",0 cnop 0,4 c_layersname: dc.b "layers.library",0 cnop 0,4 c_iconname: dc.b "icon.library",0 cnop 0,4 c_gfxbase: dc.l 0 ; brauchen wir ab und zu! dc.w 0 Nun folgt die Routine, die von der Initialisierungsroutine des Systems zusätzlich aufgrufen wird. ***************************************************************** * * * Initialisierungsroutine der Library * * * ***************************************************************** LibInitRoutine: move.l a5,-(a7) move.l d0,a5 move.l a6,sb_SysLib(a5) move.l a0,sb_SegList(a5) lea c_dosname(pc),a1 moveq #0,d0 jsr _LVOOpenLibrary(a6) move.l d0,sb_DosLib(a5) bne.B dos_ok ALERT AG_OpenLib!AO_DOSLib dos_ok: lea c_intname(pc),a1 moveq #37,d0 jsr _LVOOpenLibrary(a6) move.l d0,sb_IntuiLib(a5) bne.B int_ok ALERT AG_OpenLib!AO_Intuition int_ok: move.l a5,d0 move.l (a7)+,a5 rts Sie sehen nun die libraryinterne Routine Open: Zuerst wird die Anzahl der Öffnungsaktionen hochgezählt und das Flag gelöscht, das ein Entfernen der Library zuläßt. ********************************* * * * Libinterne Routine OPEN * * * ********************************* Open: addq.w #1,LIB_OPENCNT(a6) bclr #LIBB_DELEXP,sb_Flags(a6) move.l a6,d0 rts Es folgt nun das Gegenstück CLOSE. Der Zähler wird um 1 verringert. Ist der Zähler auf Null, wird geprüft, ob das DELEXP-Flag erlaubt, die Library zu entfernen. Wenn ja, wird zu "Expunge" gesprungen und die Library aus dem Speicher entfernt. ********************************* * * * Libinterne Routine CLOSE * * * ********************************* Close: moveq #0,d0 subq.w #1,LIB_OPENCNT(a6) bne.B endclose btst #LIBB_DELEXP,sb_Flags(a6) beq.B endclose bra.B Expunge endclose: rts Expunge prüft auf Erlaubnis, die Library zu entfernen. Dann werden die geöffneten Librarys geschlossen und die Library mittels Remove entfernt. ********************************* * * * Libinterne Routine EXPUNGE * * * ********************************* Expunge: movem.l d2/a5/a6,-(a7) move.l a6,a5 move.l sb_SysLib(a5),a6 tst.w LIB_OPENCNT(a5) beq.B noopen bset #LIBB_DELEXP,sb_Flags(a5) moveq #0,d0 bra.B ExpungeEnd noopen: move.l sb_SegList(a5),d2 move.l a5,a1 jsr _LVORemove(a6) move.l sb_DosLib(a5),a1 jsr _LVOCloseLib(a6) move.l sb_IntuiLib(a5),a1 jsr _LVOCloseLib(a6) moveq #0,d0 move.l a5,a1 move.w LIB_NEGSIZE(a5),d0 sub.l d0,a1 add.w LIB_POSSIZE(a5),d0 jsr _LVOFreeMem(a6) move.l d2,d0 ExpungeEnd: movem.l (a7)+,d2/a5/a6 rts ExtFunc ist noch unbenutzt und wird mit einer sinnlosen Funktion belegt. ********************************* * * * Libinterne reservierte Function * * ********************************* ExtFunc: moveq #0,d0 rts Nun folgt unsere erste Routine, welche die erwähnten Librarys öffnet. Als Ergebnis erhalten wir einen Zeiger auf die Librarybases in der Reihenfolge exec,graphics,intuition,layers,dos und icon. Warum Exec? Nun, zwar ist die Execbase in der Adresse 4 schon zu finden, aber auf eine Kopie im FastRAM haben wir schnelleren Zugriff! Parameter werden keine benötigt. Wegen "AllocVec" wird OS 2.0 hier vorrausgesetzt. Wenn Sie kein OS 2.0 haben, so dürfen Sie Ihr Können gleich üben: Versuchen Sie, AllocVec mit einer AllocMem-Sequenz zu ersetzen! AllocMem und AllocVec sind im Prinzip gleich. Nur müssen Sie sich bei AllocVec die Größe nicht merken. Beim Aufruf von FreeVec genügt die Adresse, während Sie bei FreeMem auch noch die Größe wissen müssen, die der Speicherbereich hatte! ******************************** * * Routine OpenAllLibs * Öffnet GFX,INT,LAYERS,DOS und ICON * * * Parameter: Keine * Rückgabe: in D0 Zeiger auf Liste der Bases * in der Reihenfolge exec,gfx,int,layers,dos und icon * Ein leerer Zeiger heisst: Lib konnte nicht * geöffnet werden. * Eine Null in d0: Wegen Speichermangel ganz unmöglich! * Es werden mindestens V37-Librarys verlangt * (Kann von Ihnen geändert werden) * Die Execbase selbst wird auf übergeben, da das * dieser aus dem FastMem schneller ist als von * Adresse 4 * ********************************* OpenAllLibs: movem.l a4/a5/a6,-(a7) ; Benutzte Register retten move.l 4.w,a6 ; Execbase moveq #24,d0 ; Speicher holen für 6 Zeiger move.l #$30001,d1 ; Gelöschtes FastMem am Stück jsr _LVOAllocVec(a6) ; Speicher holen move.l d0,a5 ; Adresse merken move.l d0,a4 ; Kopie... move.l a5,d0 ; Testen, ob erfolglos beq.B fehler ; ging schief, also Null nach d0 move.l 4.W,(a5)+ ; Execbase als erstes in die Liste lea c_gfxname(pc),a1 ; Name Graphicslibrary moveq #37,d0 jsr _LVOOpenLibrary(a6) ; GFXLIBRARY öffnen move.l d0,(a5)+ ; GFXBase lea c_intname(pc),a1 moveq #37,d0 jsr _LVOOpenLibrary(a6) ; INTUITIONLIBRARY öffnen move.l d0,(a5)+ ; INTBASE lea c_layersname(pc),a1 moveq #37,d0 jsr _LVOOpenLibrary(a6) ; LAYERSLIBRARY öffnen move.l d0,(a5)+ ; LAYERSBASE lea c_dosname(pc),a1 moveq #37,d0 jsr _LVOOpenLibrary(a6) ; DOSLIBRARY öffnen move.l d0,(a5)+ ; DOSBASE lea c_iconname(pc),a1 moveq #37,d0 jsr _LVOOpenLibrary(a6) ; ICONLIBRARY öffnen move.l d0,(a5) ; ICONBASE move.l a4,d0 ; Adresse der Liste als Ergebnis fehler: movem.l (a7)+,a4/a5/a6 ; Zurück rts ; Das wars! Unsere nächste Routine soll die geöffneten Librarys wieder schließen. Als Parameter übergeben wir den gleichen Zeiger, den wir bei OpenAllLibs erhalten haben nach a1. Die Routine schließt dann die Library wieder und gibt den Speicher für die Liste wieder frei. Beachten Sie, wie peinlich genau die Routine jeden Zeiger erst prüft! Schließlich soll ja kein CloseLibrary aufgerufen werden, wenn im Librarybase-Zeiger gar nichts steht! ********************************* * * Alle mit OpenLibAll geöffneten Librarys wieder schließen * * Übergabe des von OpenAllLibs geöffneten Zeigers nach a1! * * ********************************* CloseAllLibs: movem.l d7/a2/a3/a6,-(a7) ; Register retten move.l a1,a2 ; Parameter retten move.l a2,a3 ; dto. move.l (a2)+,a6 ; execbase braucht kein CloseLib move.l (a2)+,d7 ; GFXBASE beq.B keine_gfx ; keine da... move.l d7,a1 ; Nach a1 jsr _LVOCloseLibrary(a6) ; und schließen keine_gfx: move.l (a2)+,d7 ; INTBASE beq.B keine_int ; keine da... move.l d7,a1 ; Nach a1 jsr _LVOCloseLibrary(a6) ; und schließen keine_int: move.l (a2)+,d7 ; LAYERSBASE beq.B keine_layers ; keine da... move.l d7,a1 ; Nach a1 jsr _LVOCloseLibrary(a6) ; und schließen keine_layers: move.l (a2)+,d7 ; DOSBASE beq.B keine_dos ; keine da... move.l d7,a1 ; Nach a1 jsr _LVOCloseLibrary(a6) ; und schließen keine_dos: move.l (a2),d7 ; ICONBASE beq.B keine_icon ; keine da... move.l d7,a1 ; Nach a1 jsr _LVOCloseLibrary(a6) ; und Schließen keine_icon: move.l a3,a1 ; Speicher der Liste jsr _LVOFreeVec(a6) ; freigeben movem.l (a7)+,d7/a2/a3/a6 ; Ende rts Bei der nächsten Routine schlägt wieder ein Lerneffekt zu. Denn bei unserem "AutoRequestFront" müssen viele Dinge beachtet werden. Unsere Routine soll den vordersten Screen ermitteln. Nach dem dies geschehen ist, wird verhindert, daß während unser Programm so schön arbeitet, der vorderste Screen wechselt. Anschließend holen wir uns die Struktur des ersten zugehörigen Windows. Ist kein Window vorhanden, wird unsere Routine mit einer Fehlerrückgabe (0 in D0) verlassen. Ansonsten startet nun der AutoRequest. Hier einigen wir uns auf folgendes: Bei "Cancel" übergeben wir eine -1, bei Okay eine +1. Das ist zwar anders als beim echten "AutoRequest", aber wir brauchen die Null ja für unsere Fehlermeldung! Die IntuitionLibrary müssen wir nicht extra öffnen, die hat unsere Library schon beim Initialisieren geöffnet und die Basis in unsere Libbase mit dem Namen "sb_IntuiLib" abgelegt! ****************************************** * Wie AutoRequest, jedoch automatisch * auf vordersten Screen! ****************************************** AutoRequestFront: movem.l d2-d7/a2-a6,-(a7) ; Register retten move.l sb_IntuiLib(a6),a6 ; IntuitionBase jsr _LVOLockIBase(a6) ; IntuitionBasis locken move.l d0,d7 ; Lock merken move.l 60(a6),a5 ; Der vorderste Screen move.l 4(a5),d6 ; Das erste Window beq.B kein_fenster ; Fehler! move.l d7,a0 ; IntLock jsr _LVOUnlockIBase(a6) ; Lock befreien move.l d6,a0 ; Nach a0 lea bodytext(pc),a1 ; Bodytext Requester lea lefttext(pc),a2 ; Linker Text lea righttext(pc),a3 ; Rechter Text moveq #0,d0 moveq #0,d1 move.w #320,d2 ; Nur bis Kick 1.3 move.w #160,d3 ; Nur bis Kick 1.3 jsr _LVOAutoRequest(a6) tst.l d0 beq.B war_rechts ; Rechts angeklickt moveq #1,d0 bra.B vorbei ; Das war es war_rechts: moveq #-1,d0 bra.B vorbei *---------------------- kein_fenster: moveq #0,d0 vorbei: movem.l (a7)+,d2-d7/a2-a6 rts *---------------------- bodytext: dc.b 0,1 ; Farben dc.b 0 ; Modus dc.b 0 ; Füller dc.w 10,10 ; Textposition dc.l 0 ; Standardfont dc.l bodystring ; Der Text dc.l 0 ; Kein weiterer Text bodystring: dc.b "Sind Sie sich da sicher ?",0 cnop 0,4 lefttext: dc.b 0,1 dc.b 0 dc.b 0 dc.w 5,3 dc.l 0 dc.l leftstring dc.l 0 leftstring: dc.b "Ja!" cnop 0,4 righttext: dc.b 0,1 dc.b 0 dc.b 0 dc.w 5,3 dc.l 0 dc.l rightstring dc.l 0 rightstring: dc.b "Nein!" cnop 0,4 Unsere nächste Routine verrät Ihnen zugleich, wie Sie die Größe einer Datei ermitteln können. Ab dann können Sie es ja bequem per Library automatisieren. Diesen Part erklären wir nun etwas ausführlicher. Zunächst beginnen wir der Ordnung halber so: **************************************************** * * CheckFileSize ermittelt Größe einer Datei * * Parameter: In a0 Zeiger auf Pfad * Ergebnis: In d0 Größe oder 0 * **************************************************** CheckFileSize: link a5,#-32 ; Für lokale Variablen! movem.l d2-d7/a2-a4/a6,-(a7) ; Register retten Unsere Librarybase brauchen wir noch, A6 aber auch. Deshalb retten wir sie auf unserem lokalen Stack. Wir müssen nämlich Speicher holen für den FileInfoBlock! move.l a6,-4(a5) Jetzt holen wir die benötigten 260 Bytes für den Infoblock: move.l 4.W,a6 move.l #260,d0 moveq #1,d1 jsr _LVOAllocVec(a6) move.l d0,-8(a5) ; Merken beq.B error Zunächst muß unsere Routine den Lock der Datei besorgen, dessen Zeiger auf den Pfad wir nach a0 übergeben haben. Die Dosbase befindet sich schon innerhalb unserer eigenen Librarybase. Holen wir sie uns also: move.l -4(a5),a6 ; Unsere Libbase move.l sb_DosLib(a6),a6 ; Dosbase move.l a0,d1 ; Der Pfad moveq #-2,d2 ; Lock zum Lesen jsr _LVOLock(a6) ; Lock holen move.l d0,-12(a5) ; Unser lokaler Stack beq.B fehler2 ; Das ging wohl schief.... Jetzt füllen wir den Fileinfoblock mit unseren gewünschten Daten. move.l -8(a5),d2 ; Zeiger Infoblock move.l d0,d1 ; Lock jsr _LVOExamine(a6) ; Infoblock füllen Der FileInfoBlock ist nun richtig mit Daten gefüttert oder im Fehlerfalle leer. So oder so müssen wir den Lock jetzt wieder freigeben. Wir haben im Prinzip ja, was wir wollen! move.l -12(a5),d1 jsr _LVOUnLock(a6) Jetzt testen wir unseren FileInfoBlock auf Erfolg: move.l -8(a5),a3 move.l 124(a3),d6 ; Dateigröße vorläufig mal nach d6 beq.B fehler2 ; Das ging schlief Den Speicher für den Infoblock können wir nun wieder freigeben: move.l 4.W,a6 move.l -8(a5),a1 jsr _LVOFreeVec(a6) move.l d6,d0 bra.B kein_fehler ; d0 enthält nun Dateigröße *************************************************** * Fehlerbehandlungen *************************************************** fehler2: move.l 4.W,a6 move.l -8(a5),a1 jsr _LVOFreeVec(a6) error: moveq #0,d0 kein_fehler: movem.l (a7)+,d2-d7/a2-a4/a6 unlk a5 rts ************************************************************* * * Kennung Libraryende * ************************************************************* EndLib: END Es folgt nur noch die Kennung für das Libraryende, und unsere Library ist somit fertig. Wir können Sie assemblieren, "spezial.library" taufen und in das Libs-Verzeichnis kopieren. Wenn Sie beim Assemblieren Fehlermeldungen bekommen, so sollten Sie prüfen, ob Ihr Assembler mit dem Code was anfangen kann und ob Sie wirklich alle Includes richtig eingebunden haben. Ich habe dieses Programm erprobt und als Beweis ist genau diese Library auf der beiliegenden Diskette, fix und fertig. Ich hoffe, daß Sie nun eine gute Anregung für Ihre eigene Library gefunden haben und wünsche Ihnen viel Spaß dabei! Besonders nützliche und gute Librarys sollten Sie dem Public-Domain - oder Shareware-Pool zur Verfügung stellen! KAPITEL 6 Fragen und Antworten Immer wieder tauchen Fragen auf, und was ist die logische Konsequenz, wenn man keinen Experten zur Hand hat? Man versucht sich durch Zeitschriften zu wühlen, um die Antwort zu finden. Aus diesem Grunde habe ich einige häufige Fragen zusammengetragen, um diese hier zusammenfassend zu beantworten. Die Schwierigkeitsgrade sind dabei sehr unterschiedlich. Legen wir also los: FRAGE: Bei manchen Spielen oder Demos legt man die Diskette ein, und blitzschnell erscheint ein Bild auf dem Monitor. Wie ist das möglich? ANTWORT: Der Geschwindigkeitskniff liegt darin, daß das Bild mit Hilfe einer Routine innerhalb des Bootblock der Diskette geladen wird. Das Bild wird dann ab Sektor 2 direkt - oft auf ohne Umweg über eine Directory - mit Hilfe des Trackdisk-Devices geladen. Oft paßt ein solches Bild auf den Rest des Zylinders 0, manchmal wird noch Zylinder 1 gebraucht. Jedenfalls ist ruckzuck und ohne Stepperorgien des Schreib/Lese-Kopfes das Bild geladen. Sie müssen also Aufpassen: Das Spielchen ist nur sicher bei eh directorylosen Disketten. Sie speichern nun Ihr Bild ab Block 2 ab, also direkt nach dem Bootblock, und lesen es mit DoIO wieder ein. Für den Normalanwender erscheint es dann so, als wäre das ganze Bild in den Bootblock gequetscht worden. Die Checksumme des Bootblocks berechnet sich übrigens so: lea Bootblock,a0 lea 4(a0),a1 clr.l (a1) moveq #255,d1 moveq #0,d0 rechne: add.l (a0)+,d0 bcc.b schleife addq.l #1,d0 schleife: dbf d1,rechne not.l d0 move.l d0,(a1) FRAGE: Bei meinem selbstgeschriebenen IFF-Lader wird bei einigen Bildern nur Salat angezeigt. ANTWORT: Das kann natürlich an vielem liegen. Beispielsweise an einer falsch berechneten Bytebreite. Nehmen wir an, das wir die Breite in Pixeln im Register d0 haben, dann errechnet sich die Bytebreite wie folgt: add.w #15,d0 lsr.w #4,d0 add.w d0,d0 Genausogut können Sie aber auch im Falle eines Hardwarehacks die Register falsch initialisiert haben! FRAGE: Wie muß mein Programm beginnen, damit ich es sowohl von der Workbench als auch vom CLI aus starten kann? Wenn ich mein Programm von der Workbench aus starten will, erscheint ein GURU! ANTWORT: Es muß im Falle eines Workbenchstartes die WB-Message abgeholt und beantwortet werden, sonst führt der Messagestau zum Guru. Der Programmkopf sieht (mindestens) so aus: start: move.l 4.w,a6 suba.l a1,a1 jsr _LVOFindTask(a6) ; Eigenen Task suchen move.l d0,a4 ; Taskstruktur merken tst.l 172(a4) ; Start vom CLI ? bne.B vom_cli ; Ja lea 92(a4),a0 ; Auf Startup-Message warten jsr _LVOWaitPort(a6) ; Warten... lea 92(a4),a0 ; Startup-Message jsr _LVOGetMsg(a6) ; Message lesen move.l d0,wb_message ; Startup-Message merken vom_cli: . . . Die Message sollte natürlich noch replyt werden, spätestens kurz vor Verlassen des Programmes! FRAGE: Wie kann man per Programm feststellen, ob sich der AA-Chipsatz im Rechner befindet? ANTWORT: Des Rätsels Lösung finden Sie in der GfxBase. Dort befindet sich an Offset 236 ein Byte namens "ChipRevBits". Den AA-Chipsatz erkennen Sie daran, daß sich dort $1f befindet, sofern der Chipsatz aktiviert ist. FRAGE: Wie kann ich am einfachsten einem selbstgeschriebenen CLI-Befehl Parameter übergeben ? ANTWORT: Im Register d0 steht automatisch die Größe in Bytes Ihrer Parameter und im Register A0 die Adresse. Nehmen wir an, es handelt sich bei dem Parameter um einen Dateipfad. Dann könnte die Routine folgendermaßen aussehen: start: subq.w #1,d0 bne.B argumente sense: rts ; Keine Argumente argumente: cmp.b #$20,(a0)+ bne.B hier dbf d0,argumente bra.B sense hier: subq.l #1,a0 lea pfad(pc),a4 pfadschreiber: move.b (a0)+,(a4)+ dbf d0,pfadschreiber clr.b -1(a4) FRAGE: Ich benötige einen einigermaßen guten Requester, der aber nicht nur 2 Werte abfragt wie zum Beispiel "AutoRequest", sondern quasi beliebig viele! ANTWORT: Hierfür ist die Routine "EasyRequestArgs" der "intuition.library" mit dem Offset -588 gedacht. Der Aufruf: Window: a0 EasyStruktur: a1 IDCMP: a2 Argumente: a3 In A0 muß die Adresse des betroffenen Windows. Nach a1 muß der Zeiger einer Easy-Requester-Struktur. Der Zeiger auf eventuelle zusätzliche IDCMP-Flags darf auch leer sein. A3 schließlich sollte auf einen leeren Datenbereich für z.B. einen Dateinamen zeigen. Wichtig: Das Gagdet ganz rechts entspricht 0, also dem CANCEL wie bei AutoRequest! move.l intbase(pc),a6 move.l 60(a6),a5 ; Erster Screen move.l sc_FirstWindow(a5),a0 ; Erstes Fenster lea ChangeReq(pc),a1 ; Fragen stellen lea IDCMP(pc),a2 ; IDCMP-Zeiger lea Parameter(pc),a3 jsr _LVOEasyRequestArgs(a6) cmp.w #1,d0 beq.B aber_ja ; 1. Gadget cmp.w #2,d0 beq.B warum ; 2. Gadget cmp.w #3,d0 beq.W heute_nicht ; 3. Gadget cmp.w #4,d0 beq.B vielleicht ; 4. Gadget cmp.w #5,d0 beq.B haeeh ; 5. Gadget bra.W nein ; Muß in die Datei !! *------------------------------------------------------------------------- IDCMP: dc.l 0 ChangeReq: dc.l es_SIZEOF,0,_Title,_Fmt,_Buttons _Title: dc.b "Witziger Laderequester ",0 cnop 0,4 _Fmt: dc.b "Soll ich die Datei einladen ?",0 cnop 0,4 _Buttons: dc.b "ABER JA! |WARUM? | HEUTE NICHT | VIELLEICHT | HAEH? | NEIN",0 cnop 0,4 Parameter: blk.b 256,0 ; Eventueller Dateiname FRAGE: Wenn ich aus einer Screenstruktur die Mauskoordinaten abfrage, bekomme ich völlig falsche Werte! ANTWORT: Sie haben bestimmt die Koordinaten verwechselt. Man geht nämlich normalerweise davon aus, daß zuerst die X-Koordinate und dann die Y-Koordinate folgt. In der Screenstruktur ist es aber umgekehrt: Beim Offset 16 steht die Y-Koordinate, beim Offset 18 die X-Koordinate. Der Hitpoint wird aber in der Screenstruktur NICHT berücksichtigt! Hier müssen Sie die Mauskoordinaten im gerade aktiven Window benutzen! Dort steht bei Offset 12 die relative Y-Position zum Window, und bei 14 die relative X-Position. Der Hitpoint wird durch zwei BYTE-Werte dargestellt, nämlich für X der Offset 80 und für Y der Offset 81 (Hoppla, da stimmt die Reihenfolge plötzlich wieder!) FRAGE: Was ist der Unterschied zwischen AllocMem und AllocVec? Die zu übergebenden Parameter sind doch gleich! ANTWORT: Richtig. Aber Sie müssen sich nur noch noch die Adresse merken, nicht aber die Größe. Um den mit AllocVec geforderten Speicher freizugeben, genügt die Adresse in A1 und FreeVec, während Sie bei FreeMem noch die Größe wissen mußten. Hier übrigens nochmal ein kleines Progrämmchen, daß die Benutzung von AllocVec() demonstriert! incdir "include:" include "exec/exec.i" include "lvo/exec.i" include "dos/dos.i" include "lvo/dos.i" AllocVec1: move.l 4,a6 move.l #MEMF_FAST,d1 move.l #480000,d0 jsr _LVOAllocVec(a6) tst.l d0 beq.w errende1 move.l d0,MemBase1 move.l 4,a6 move.l #MEMF_FAST,d1 AllocVec2: move.l #480000,d0 jsr _LVOAllocVec(a6) tst.l d0 beq.w errende1 move.l d0,MemBase2 move.l #DosName,a1 move.l #37,d0 jsr _LVOOpenLibrary(a6) tst.l d0 beq.w errende1 move.l d0,DosBase PutText2: move.l DosBase,a6 move.l #FuellText,d1 jsr _LVOPutStr(a6) Fuellen1: move.l #10,d1 ; Erstes Bild mit 10 füllen move.l #479999,d2 move.l MemBase1,a0 loop1: move.b d1,(a0)+ dbf d2,loop1 Fuellen2: move.l #1,d1 ; Zweites Bild mit 1 füllen move.l #479999,d2 move.l MemBase2,a0 loop2: move.b d1,(a0)+ dbf d2,loop2 move.l #25,d3 ; 25 Bilder pro Sekunde :) PutText3: move.l DosBase,a6 move.l #AddText,d1 jsr _LVOPutStr(a6) Addieren: move.l #479999,d2 move.l MemBase1,a0 move.l MemBase2,a1 loop3: move.b (a1)+,d1 ; Bildveränderung in d1 add.b d1,(a0)+ ; zum vorherigen Bild dazu addieren dbf d2,loop3 dbf d3,Addieren ; nächstes Bild NormalesEnde: move.l DosBase,a6 move.l #Text4,d1 jsr _LVOPutStr(a6) move.l 4,a6 move.l DosBase,a1 jsr _LVOCloseLibrary(a6) move.l MemBase1,a1 jsr _LVOFreeVec(a6) move.l MemBase2,a1 jsr _LVOFreeVec(a6) move.l #0,d0 rts errende1: move.l DosBase,a6 move.l #ErrorText1,d1 jsr _LVOPutStr(a6) move.l 4,a6 move.l DosBase,a1 jsr _LVOCloseLibrary(a6) rts MemBase1: dc.l 0 MemBase2: dc.l 0 DosName: DOSNAME cnop 0,4 DosBase: dc.l 0 ErrorText1: dc.b "Konnte Speicher nicht allocieren oder Dos nicht öffnen!",0 cnop 0,4 FuellText: dc.b "Fülle 480000 Bytes mit 10 !",$0A,0 cnop 0,4 AddText: dc.b "Addiere 25*480000mal 20 dazu !",$0A,0 cnop 0,4 Text4: dc.b "Fertig !",$0A,0 cnop 0,4 END FRAGE: Wie stelle ich fest, welcher Prozessor im Rechner ist? ANTWORT: In der Execbase bei Offset 296 (AttnFlags) steht ein Wort, das Ihnen die gewünschte Auskunft gibt. Kein Bit gesetzt: 68000 und sonst nichts. Bit 0 gesetzt: 68010 (auch bei 68020 gesetzt) Bit 1 gesetzt: 68020 (auch bei 68030 gesetzt) Bit 2 gesetzt: 68030 (auch bei 68040 gesetzt) Bit 3 gesetzt: 68040 Bit 4 gesetzt: 68881-Coprozessor (auch bei 68882 gesetzt) Bit 5 gesetzt: 68882 Warum zwischen 68881 und 68882 unterschieden werden muß, weiß ich selbst leider nicht. Meines Wissens nach sind beide funktionell identisch. Kurioserweise läuft IMAGINEFP nur mit dem 68882 korrekt und schnell (und natürlich mit dem 68040). FRAGE: Was sind eigentlich TAGS, speziell bei Intuition ? ANTWORT: Tag heißt zu deutsch "Anhängsel". Bisher wurden große Parameter in Form von Strukturen übergeben die meist eine starre Größe hatten und nur schwer erweiterbar sind. Anders bei den Tags: Hier werden nur die Parameter übergeben, die auch wirklich gebraucht werden, alle anderen sind mit einem Defaultwert vorbelegt. Selbst die Reihenfolge der Parameter ist egal geworden. Um das zu ermöglichen, wird zuerst die betroffene Tag-Kennung und dann der Parameter geschrieben. "OpenScreenTagList" beispielsweise ist ein Ersatz für OpenScreen und darf theoretisch sogar völlig ohne Parameter aufgerufen werden. In diesem Fall wird ein Lores-Screen mit 2 Farben geöffnet. Leider würde dieses Buch den Rahmen sprengen, um genauer auf die Tags einzugehen. Weiterführende Literatur ist vonnöten. Aber trotzdem hier ein Beispiel für OpenScreenTagList und OpenWindowTaglist! Das Listing ist erst lauffähig, wenn Sie es auf Ihre Verhältnisse (Includes etc.) angepaßt haben! incdir "include:" incdir "include:lvo" include "exec/exec.i" include "exec.i" include "intuition/intuition.i" include "intuition/intuition_lib.i" include "intuition/screens.i" include "graphics/displayinfo.i" include "intuition.i" Section Programm,Code move.l 4,a6 OpenIntui: move.l #37,d0 move.l #IntuiName,a1 jsr _LVOOpenLibrary(a6) tst.l d0 beq errende move.l d0,IntuiBase OpenTagScreen: move.l #0,a0 move.l #ScreenTagList,a1 move.l IntuiBase,a6 jsr _LVOOpenScreenTagList(a6) move.l d0,ScreenHandle OpenTagWindow: move.l IntuiBase,a6 move.l #0,a0 move.l #WindowTagList,a1 move.l #ScreenHandle,Custom jsr _LVOOpenWindowTagList(a6) tst.l d0 beq ScreenClose move.l d0,WindowHandle CloseTagWindow: move.l WindowHandle,a0 jsr _LVOCloseWindow(a6) ScreenClose: move.l ScreenHandle,a0 jsr _LVOCloseScreen(a6) errende: move.l -5,d0 rts Section Daten,Data IntuiName: INTNAME IntuiBase: dc.l 0 ScreenHandle: dc.l 0 ScreenTagList: dc.l SA_Width,640 dc.l SA_Height,512 dc.l SA_Depth,4 dc.l SA_DisplayID,HIRESLACE_KEY dc.l SA_Title,ScreenTitle dc.l TAG_DONE,0 ScreenTitle: dc.b "Mein erster Screen",0 cnop 0,4 WindowHandle: dc.l 0 WindowTagList: dc.l WA_ScreenTitle,ScreenTitle Custom: dc.l WA_CustomScreen,ScreenHandle dc.l WA_Left,10 dc.l WA_Top,100 dc.l WA_Width,200 dc.l WA_Height,300 dc.l WA_Title,WindowTitle dc.l TAG_DONE,0 WindowTitle: dc.b "Mein Window",0 cnop 0,4 END KAPITEL 7 DER ABSOLUT SICHERE KOPIERSCHUTZ Über den Sinn und Unsinn eines Kopierschutzes mag man streiten. Nichtsdestotrotz macht es Spaß, sich mit dieser Materie zu befassen. Warum also nicht? Die Überschrift täuscht natürlich ein wenig. Zwar läßt sich ein Kopierschutz entwickeln, den kein Kopierprogramm der Welt überwinden kann, aber deshalb ist der Schutz nicht absolut sicher. Schließlich gibt es ja da noch die heißgeliebten Cracker, die nur darauf warten, diesen wieder zu entfernen. Aber es gibt Schutzmethoden, die selbst heutige Hardwarezusätze nicht überwinden können, weil es rein physikalisch einfach unmöglich ist! Die Grundlagen Das erste für uns interessante Register ist das Drivestatus-Register $BFE001, das den meisten bekannt ist, weil dort z.B. auch die Feuerknöpfe bzw. die linke Maustaste abgefragt wird. Für den Diskettenbetrieb sind die Bits 2-5 von Bedeutung: (Vergessen Sie nicht, daß die Bits ab 0 gezählt werden. Bit 2 ist demnach das dritte Bit.) Bit 2 Ist dieses Bit gesetzt, so ist eine Diskette im Laufwerk. Das Bit wird aber erst aktualisiert, wenn der Steppermotor bewegt wird. Bit 3 Wenn die Diskette schreibgeschützt ist, so ist das Bit gelöscht. Bit 4 Befindet sich der Schreib/Lesekopf auf Zylinder 0, so ist dieses Bit gelöscht. Bit 5 Man kann das Diskettenlaufwerk nicht mit Kommandos "Überschütten": Erst wenn Bit 5 auf 0 ist, kann das angewählte Laufwerk Befehle annehmen. DRIVE SELECT Um nun den Kontroller zu bedienen, benutzen wir das CIA B - Register $BFD100, welches einige wichtige Funktionen beinhaltet. Wie alle CIA-Register hat auch dieses eine Größe von 8 Bit. Erstens müssen wir eine Möglichkeit haben, das Laufwerk auszuwählen, denn bis zu vier Floppys können angeschlossen sein. Zweitens brauchen wir einen "Schalter" für den Laufwerksmotor und eine Steuerung des Steppermotors. Doch nun die Belegung im einzelnen: Bit 7: Motorbit Um gezielt den Motor eines der vier Laufwerke einzuschalten, müssen wir folgendermaßen vorgehen: Zunächst deselektieren wir alle vier Laufwerke und setzen dieses Bit auf 0. Nun selektieren wir ein Laufwerk. Dadurch schaltet sich der Motor des gewünschten Drives ein. Bei gesetztem Bit wird dagegen bei dieser Prozedur der betreffende Motor AUS-Geschaltet. Bit 6: Laufwerk 3 Bit 5: Laufwerk 2 Bit 4: Laufwerk 1 Bit 3: Laufwerk 0 Bit 2: Ist dieses Bit auf 0, wird die obere Spur gelesen/geschrieben, bei 1 die untere. Bit 1: Wenn der Schreibkopf bewegt wird, so bestimmt dieses Bit die Richtung. Bei 0 wird der Kopf nach innen bewegt, bei 1 nach außen. Bit 0: Dieses Bit sendet die Stepimpulse, löst also die Bewegung des Schreibkopfes aus. Dies geschieht durch Löschen des Bits. Nach dem Löschen sollte es sofort wieder gesetzt werden, schon alleine deshalb, um weitersteppen zu können. ACHTUNG: Die Stepgeschwindigkeit sollte nicht übertrieben werden, sonst kommt das Laufwerk nicht mehr mit. Benutzen Sie aber auf keinen Fall DBF-Schleifen oder ähnliches für die Pause, denn sonst versagt Ihre Steproutine auf Turbokarten. Um ein wenig in Übung zu kommen: Hier nun ein kleiner Hardwarehack, der im Laufwerk DF0: zuerst das Vorhandensein einer Disk prüft, dann den Kopf zu Zylinder 0 fährt und anschließend zu Cylinder 79. Danach fährt der Kopf wieder zum ursprünglichen Ausgangspunkt. Stepübung: drive equ $bfd100 status equ $bfe001 start: lea $dff000,a5 move.w #$4000,$9a(a5) ; IRQ aus move.b #$ff,drive , Alles auf High ; ; Auf eingelegte Diskette warten ; wait_of_disk: bsr.B innenstep ; 1 Spur nach innen move.l #500,d5 ; etwas warten bsr.W pause bsr.B aussenstep ; 1 Spur nach aussen move.l #500,d5 bsr.W pause btst #2,status ; Diskette drin ? beq.B wait_of_disk ; ; Auf Spur 0 fahren und Steps mitzählen ; moveq #0,d4 steppo: btst #4,status ; Spur 0 ? beq.B am_ziel ; ja bsr.W aussenstep ; nein addq.w #1,d4 ; Mitzählen moveq #2,d5 ; Ganz kurz warten bsr.W pause bra.B steppo am_ziel: moveq #78,d2 ; Zähler für 79 Zylinder loop: bsr.B innenstep ; 1 Spur nach innen moveq #2,d5 bsr.W pause dbf d2,loop moveq #79,d2 sub.w d4,d2 ; wieder zur Normalspur loop2: bsr.B aussenstep moveq #2,d5 bsr.W pause dbf d2,loop2 aus: move.w #$c000,$9a(a5) rts innenstep: bclr #1,drive bclr #0,drive bset #0,drive rts aussenstep: bset #1,drive bclr #0,drive bset #0,drive rts pause: move.l $dff004,d7 and.l #$0001ff00,d7 cmp.l #$0001f000,d7 bhi.B pause lsr.l #8,d7 move.b d7,d6 add.b #15,d6 sleep: cmp.b $dff006,d6 bne.B sleep dbf d5,pause rts Soweit, so gut. Aber wir wollen ja nicht nur mit dem Steppermotor spielen, sondern Daten lesen und schreiben. Auch der Diskettenbetrieb arbeitet mittels DMA, und zwar völlig prozessorunabhängig, da nur gerade Zyklen benutzt werden. Der Prozessor muß nur die Register initialisieren. Natürlich müssen die Daten beim Schreiben noch MFM-Codiert werden (mit Blitter möglich!) oder beim Lesen decodiert. Aber der Datentransfer läuft "wie von selbst!" Die Daten, die von der Disk-DMA gehandhabt werden, müssen im CHIP-Memory liegen. Die Adresse, ab der geschrieben bzw. wohin gelesen wird, kommt in das DSKPT-Register $DFF020 und hat im Prinzip Langwortformat. Das nächste zu besprechende Register ist DSKLEN mit der Adresse $DFF024 und hat folgende Bedeutung: Bit: 15 1 = DMA ist möglich 14 1 = schreiben 0 = lesen 13-0 = Anzahl der zu schreibenden Worte. WICHTIG: Das Register wird als LETZTES beschrieben, um die Disk-DMA zu starten. Als weitere Besonderheit gilt: Das Register muß zweimal beschrieben werden, um die Aktion wirksam werden zu lassen. Grund: Man wollte vermeiden, daß durch einen Programmfehler versehentlich wichtige Daten durch Überschreiben zerstört werden. Bevor also DSKLEN beschrieben wird, muß noch ein anderes Register initialisiert werden, und zwar ADKCON, mit der Adresse $DFF09E. Dieses Register ist auch lesbar, aber an einer anderen Adresse, nämlich $DFF010. Die Belegung: Bit Funktion 15 1 = Die anderen gesetzten Bits werden auf 1 gesetzt 0 = Die anderen gesetzten Bits werden auf 0 gesetzt 14/13 Precomp-Zeit. 00 = Keine, 01 = 140 Ns, 10 = 280 ns 11 = 560 ns 12 1 = MFM-Format 0 = GCR-Format 11 1 = Daten werden zu Paula geschickt, 0 = Daten werden zu Diskette geschickt. 10 1 = Ein bestimmtes Wort wird als Syncmarkierung benutzt. 9 1 = GCR-Sync soll benutzt werden 8 1 = MFM-Geschwindigkeit 0 = GCR-Geschwindigkeit Das erwähnte Syncwort (signalisiert mit Bit 10) wird in das Register $DFF07E geschrieben. Sinnvollerweise muß es ein Wort sein, daß durch normale MFM-Codierung nicht entstehen kann, aber dennoch legal ist. Es hat sich hier der Wert $4489 durchgesetzt. Der Kopierschutz Unser Kopierschutz, der selbst von einem Hardwarezusatz nicht kopiert werden kann, beruht auf folgendem Prinzip: Kein Diskettenlaufwerk hat die exakt gleiche Umdrehungsgeschwindigkeit wie ein anderes. Außerdem ist der Motor noch Gleichlaufschwankungen unterworfen. Die Idee: Wir schreiben eine Spur randvoll mit Daten und zählen diese. Diesen Wert merken wir uns und benutzen ihn später, um die geschriebene Datenmenge abzufragen. Weicht die Datenmenge ab, liegt eine Kopie vor! Beim Lesen einer Spur stört die Gleichlaufschwankung nicht, aber wenn ein Programm versucht, die exakt gleiche Datenmenge zu schreiben, so ist die Chance sehr gering. Eine lauffähige Kopie wird somit zum reinen Glückstreffer, und daran ändert auch ein Hardwarezusatz nichts! Damit wir aber einen definierten Anfang auf unserer Schutzspur haben, wollen wir auch die Syncmarkierung einsetzen. Es gibt zwei Möglichkeiten, zu synchronisieren. Entweder mit dem erwähnten Wort $4489, das wir sicherheitshalber zweimal hintereinander schreiben sollten, da der Kontroller manchmal das erste verschluckt. Oder wir benutzen die Indexmarkierung. Bei jeder Umdrehung signalisiert der Motor mit Hilfe einer Lichtschranke, daß eine Umdrehung vollendet ist. Dieses Signal läßt sich abfragen. Allerdings ist das Indexverfahren relativ ungenau. Ein bitgenaues Synchronisieren ist so nicht möglich. Bytegenau reicht aber wohl aus. Die Indexmarkierung erkennen Sie durch Bit 4 im Register $BFDD00. Ist das Bit gesetzt, wurde die Markierung erreicht. Am sichersten: Sie warten bis die Markierung auftaucht, und dann, bis sie wieder beendet ist. Zu guter Letzt müssen wir noch auf einen DMA-Fehler hinweisen: Beim Schreiben wird das letzte Wort nicht mehr komplett geschrieben, beim Lesen wird das letzte Wort verschluckt. Um nun Daten zu zählen, können wir auf den Index warten, und dann solange Bytes zählen, bis diese wieder auftaucht. Die gelesenen Bytes können wir beispielsweise zusammenaddieren, um eine Checksumme zu bilden. Das haarscharfe Messen der Tracklänge ist ein vorbildlicher Kopierschutz. Hier nun die nötige Routine, welche die Bytes auf Spur 0 mit Hilfe der Indexmarkierung zählt: ; ; Trackcount ; drive equ $bfd199 status equ $bfe001 start: lea $dff000,a5 move.w #$4000,$9a(a5) ; IRQ aus move.b #$ff,drive ; Alles auf high bclr #7,drive bclr #3,drive ; Laufwerk 0 ; ; Auf eingelegte Diskette warten ; wait_of_disk: bsr innenstep move.l #500,d5 bsr pause bsr aussenstep move.l #500,d5 bsr pause btst #2,status beq.B wait_of_disk ; ; Auf Spur 0 fahren ; steppo: btst #4,status beq am_ziel bsr aussenstep moveq #2,d5 bsr pause bra.B steppo am_ziel: move.w #$7fff,$9e(a5) move.w #%1001000100000000,$9e(a5) ; ADKCON move.w #0,$24(a5) ; Keine DMA moveq #0,d7 move.b $bfdd00,d0 index: move.b $bfdd00,d0 ; Warten auf Index btst #4,d0 beq.B index index2: move.b $bfdd00,d0 btst #4,d0 bne.B index2 lesetest: move.w $dff01a,d6 btst #15,d6 beq.B lesetest addq.l #1,d7 move.b $bfdd00,d0 btst #4,d0 beq.B lesetest ; weiter, noch kein Index! aus: move.w #$c000,$9a(a5) rts ; ; Anzahl Bytes auf Spur nun in d7 ; innenstep: bclr #1,drive bclr #0,drive bset #0,drive rts aussenstep: bset #1,drive bclr #0,drive bset #0,drive rts kopfoben: bclr #2,drive rts kopfunten: bclr #2,drive rts pause: move.l $dff004,d7 and.l #$0001ff00,d7 cmp.l #$0001f000,d7 bhi.B pause lsr.l #8,d7 move.b d7,d6 add.b #15,d6 sleep: cmp.b $dff006,d6 bne.B sleep dbf d5,pause rts Nach Beendigung dieses Programms erhalten Sie die Anzahl der Bytes (noch im MFM-Code) auf der Spur. Das müssen also etwas über 12000 Bytes sein. Supergenau ist dieses Indexverfahren nicht. Eine bessere Methode ist das Einlesen einer Spur mit der $4489-Synchronisation. Wir lesen dann mit Absicht zuviel, nämlich $3800 Bytes. Die Daten, die direkt am Anfang stehen, wiederholen sich dadurch zwangsweise. Aus diesem Grund läßt sich die richtige Datenmenge leicht ermitteln. Wir suchen einfach nach der ersten Wiederholung und berechnen dann den Abstand zum Anfang. Hier nun das Listing zum Einlesen einer MFM-Codierten Spur mit Hilfe der $4489-Synchronisation: ; ; Lesen einer MFM-Spur ; drive equ $BFD100 status equ $BFE001 start: lea $dff000,a5 move.w #$4000,$9a(a5) ; IRQ aus move.b #$ff,drive bclr #7,drive bclr #3,drive ; Laufwerk 0 move.l #500,d5 bsr pause ; ; Auf eingelegte Diskette warten ; wait_of_disk: bsr innenstep move.l #500,d5 bsr.W pause bsr aussenstep move.l #500,d5 bsr pause btst #2,status beq.B wait_of_disk am_ziel: move.w #$7fff,$9e(a5) move.w #%1001010100000000,$9e(a5) ;ADKCON move.w #$4489,$7e(a5) ; Sync move.l #daten,$20(a5) ; Zeiger auf Daten move.w #$9c00,$24(a5) ; Starten move.w #$9c00,$24(a5) ; (zweimal!) warten: btst #1,$dff01f beq.B warten ; Warten, bis fertig ; ; ; Hier müßte nun die Auswertung folgen ; ; aus: move.w #$c000,$9a(a5) rts innenstep: bclr #1,drive bclr #0,drive bset #0,drive rts aussenstep: bset #1,drive bclr #0,drive bset #0,drive rts kopfoben: bclr #2,drive rts kopfunten: bclr #2,drive rts pause: move.l $dff004,d7 and.l #$0001ff00,d7 cmp.l #$0001f000,d7 bhi.B pause lsr.l #8,d7 move.b d7,d6 add.b #15,d6 sleep: cmp.b $dff006,d6 bne.B sleep dbf d5,pause rts section mfm,data_c daten: dcb.b $3800,0 Bei diesem Progrämmchen spielt das Format an sich absolut keine Rolle, solange es $4489-synchronisiert ist. Deshalb können wir nur durch Ausmessen der exakten Spurkapazität einen DOS-Kompatiblen und sehr wirksamen Kopierschutz realisieren, denn kaum ein Laufwerk schreibt exakt die gleiche Datenmenge. Um den Schutz "aufzutragen", würde es also lustigerweise genügen, die betroffene Diskette zu formatieren. ANHANG ; ; SpezialBase.i ; ; Include-File für Leidorfs Spezial-Library ; IFND SPEZIAL_BASE_I SPEZIAL_BASE_I SET 1 IFND EXEC_TYPES_I INCLUDE "exec/types.i" ENDC IFND EXEC_LISTS_I INCLUDE "exec/lists.i" ENDC IFND EXEC_LIBRARIES_I INCLUDE "exec/libraries.i" ENDC STRUCTURE SpezialBase,LIB_SIZE UBYTE sb_Flags UBYTE sb_pad ULONG sb_SysLib ULONG sb_DosLib ULONG sb_IntuiLib ULONG sb_SegList LABEL SpezialBase_SIZEOF SPEZIALNAME MACRO DC.B 'spezial.library',0 ENDM ENDC ; ; ; ; ; Spezial_lib.i Einsprünge ; _LVOOpenAllLibs EQU -30 ; Öffnen der wichtigsten Librarys _LVOCloseAllLibs EQU -36 ; Schließen derselben _LVOAutoRequestFront EQU -42 ; Autorequest auf dem vordersten Screen _LVOCheckFileSize EQU -48 ; Um Dateigröße festzustellen TTTTTTTTTTTTTTTTTTTTTTTTT IIIIIIIIIIIIII SSSSSSSSSSSSSSSSSSSSSSSSSSS TTTTTTTTTTTTTTTTTTTTTTTTT IIIIIIIIIIIIII SSSSSSSSSSSSSSSSSSSSSSSSSSS TTTTTTTTTTTTTTTTTTTTTTTTT IIIIIIIIIIIIII SSSSSSSSSSSSSSSSSSSSSSSSSSS TTTTTTTTT IIIIIIIIII SSSSSSSSSS TTTTTTTTT IIIIIIIIII SSSSSSSSSS TTTTTTTTT IIIIIIIIII SSSSSSSSSS TTTTTTTTT IIIIIIIIII SSSSSSSSSSSSSSSSSSSSSSSSSSS TTTTTTTTT IIIIIIIIII SSSSSSSSSSSSSSSSSSSSSSSSSSS TTTTTTTTT IIIIIIIIII SSSSSSSSSSSSSSSSSSSSSSSSSSS TTTTTTTTT IIIIIIIIII SSSSSSSSSS TTTTTTTTT IIIIIIIIII SSSSSSSSSS TTTTTTTTT IIIIIIIIII SSSSSSSSSS TTTTTTTTT IIIIIIIIIIIIII SSSSSSSSSSSSSSSSSSSSSSSSSSS TTTTTTTTT IIIIIIIIIIIIII SSSSSSSSSSSSSSSSSSSSSSSSSSS TTTTTTTTT IIIIIIIIIIIIII SSSSSSSSSSSSSSSSSSSSSSSSSSS