Benutzer:Berni/Rastertrick/Exakte Synchronisation

Aus C64-Wiki
Zur Navigation springenZur Suche springen

Hierbei handelt es sich nicht direkt um einen Rastertrick mit sichtbarem Effekt. Für viele Rastertricks ist allerdings eine exakte Synchronisation von Prozessor und VIC unablässige Voraussetzung, da es manchmal nur einen einzigen Taktzyklus gibt, in dem eine bestimmte Veränderung eines Registers stattfinden muss, damit der Trick funktioniert, beispielsweise bei einem horizontalen Hyperscreen.

Das Verständnis dieser Technik ist nicht ganz einfach, weshalb sie hier in mehreren Schritten erarbeitet wird.

1. Versuch (Busy-Wait-Schleife)[Bearbeiten | Quelltext bearbeiten]

Ausgabe des nebenstehenden Programms. Der Balken steht nicht still, weil die Synchronisation nicht exakt ist.

Das nachfolgende Programm ist ein erster Versuch, der das Problem deutlich macht:

*=$c000

         sei            ; Interrupt abschalten, der stört nur

         ldx #$01
         ldy #$00

--       lda $d012      ; Auf Rasterzeile 111 warten
         cmp #111
         bne --

         nop            ; Noch etwas warten, damit der Effekt
         nop            ; nicht vom Rahmen überdeckt wird
         nop
         nop
         nop
         nop

         stx $D021      ; Hintergrund weiß färben ...
         sty $D021      ; ... und gleich wieder schwarz

-        lda $d012      ; Auf Zeile 112 warten
         cmp #112
         bne -
         beq --         ; Und von Vorne beginnen

Das Programm wartet auf Rasterzeile 111 und färbt dann kurz den Hintergrund weiß und direkt danach wieder schwarz. Danach beginnt das Spiel von neuem. Die sechs NOP-Befehle sind dazu da, dass man auf dem Bildschirm was sieht, sonst würde das Hintergrundfärben teilweise vom Rahmen verdeckt.

Das Ergebnis ist leider kein stabiler Balken. Stattdessen flimmert dieser hin und her. Der Balken hat immer die Länge 4, was der Anzahl der Taktzyklen des STY-Befehls entspricht. Der Anfang des Balkens kann aber in neun verschiedenen Spalten liegen. Das liegt daran, dass die erste Schleife neun Taktzyklen benötigt und die Änderung der Rasterzeile an jedem dieser neun Taktzyklen stattfinden kann. Demnach ist dann auch die Dauer, bis der weiße Balken gezeichnet wird, unterschiedlich lang.

d011 sync1 schema.png

Man könnte die erste Schleife noch etwas kürzer machen, indem man Register und zu testende Zahl vertauscht:

--       lda #111
-        cmp $d012
         bne -

Es verbleiben so aber immer noch 6 Taktzyklen Unsicherheit und damit weiterhin ein flackernder Balken.

Zweiter Versuch (Interrupt)[Bearbeiten | Quelltext bearbeiten]

Ausgabe des nebenstehenden Programms. Der Balken steht immer noch nicht still.

Alternativ zu der obigen Warteschleife kann man auch den Rasterzeilen-Interrupt benutzen, um auf Zeile 111 zu warten:

*=$c000

         sei              ; Interrupt abschalten

         lda #111         ; Rasterzeile 111 programmieren
         sta $d012
         lda $d011
         and #$7f
         sta $d011

         lda $d01a        ; Rasterzeilen-Interrupt anschalten
         ora #$01
         sta $d01a

         lda #<irq        ; IRQ-Routine verbiegen
         sta $0314
         lda #>irq
         sta $0315

         lda #$7f         ; Timer-Interrupt ausschalten, der stört
         sta $dc0d

         cli              ; Interrupts wieder zulassen
-        bne -            ; Endlosschleife

irq      ldx #$01
         ldy #$00

         stx $D021        ; Hintergrund weiß...
         sty $D021        ; und gleich wieder schwarz

         lda $d019        ; Interrupt bestätigen
         sta $d019

         ; nop            ; siehe Text

         pla              ; Interrupt geordnet beenden
         tay
         pla
         tax
         pla
         rti

Der Strich ist weiterhin 4 Zeichen breit und überraschend weit rechts in der Zeile. Zudem wechselt das linke Ende des Balkens zwischen drei Positionen.

Ersteres liegt daran, dass die Interrupt-Routine des Kernals bereits 29 Taktzyklen benötigt, bevor unsere IRQ-Routine angesprungen wird. Hinzu kommen hier noch 8 Taktzyklen für ldx, ldy und stx, sowie weitere 7 Taktzyklen für den Aufruf des Interrupts. Es verbleiben, wenn man genau nachzählt, noch 1-3 Taktzyklen. Diese entstehen, weil der Prozessor einen Befehl, den er gerade abarbeitet, noch beendet, bevor er zum Interrupt übergeht. Hier handelt es sich um den BNE-Befehl, der, da er immer verzweigt, jedesmal 3 Taktzyklen benötigt. Je nachdem, wann der Interrupt auftritt, kommen demnach noch 1-3 Taktzyklen hinzu. (Bei anderen Befehlen kann dies bis zu 7 Taktzyklen dauern, bei illegalen Opcodes sogar bis zu 8.) Auf diesem Weg kann man demnach auch kein exaktes Timing hinbekommen.

Und noch etwas: Im Listing befindet sich ein auskommentiertes NOP. Kommentiert man dieses ein, so steht der Balken überraschenderweise still. Das liegt daran, dass diesmal die Unterbrechung dieser Endlosschleife immer zum selben Zeitpunkt stattfindet. Der Wechsel von Schleife zu Interrupt dauert dadurch immer exakt gleich lang.

War das die Lösung? Leider nein. Denn der Balken steht nicht jedesmal an der selben Stelle still. Welche Stelle er wählt, hängt vom exakten Zeitpunkt des SYS-Aufrufs ab. Den können wir aber nicht beeinflussen.

Dritter Versuch (Badline-Zustand)[Bearbeiten | Quelltext bearbeiten]

Ausgabe des nebenstehenden Programms. Der Balken steht still, aber drei graue Rechtecke sind aufgetaucht.

In den bisherigen Beispielen haben wir immer versucht, mit dem Prozessor dem VIC hinterher zu rennen. Da die Reaktionszeiten des Prozessors aber nicht exakt sind, können wir damit das Timing zwar ungefähr hinbekommen, aber eben nicht exakt. (Mit viel Aufwand wäre es sogar möglich: Man müsste dafür in mehreren aufeinanderfolgenden Zeilen die Änderung der Rasterzeilennummer abpassen und aus den so gewonnenen Informationen den genauen Zeitpunkt dieser Änderungen zurückrechnen.)

Die dritte Möglichkeit geht den umgekehrten Weg: Diesmal wird der VIC so programmiert, dass er den Prozessor synchronisiert. Und, wie wir gleich sehen werden, funktioniert das. Warum das so ist, dafür ist allerdings etwas Hintergrundwissen notwendig.

VIC und Prozessor greifen normalerweise abwechselnd auf den Speicher des Computers zu. Allerdings kann der VIC so pro Rasterzeile nur 63 mal auf den Speicher zugreifen, was nicht ausreicht, um alle für die Grafikausgabe notwendigen Informationen zu erhalten. Um dennoch an die benötigten Daten zu gelangen, greift der VIC zu einem Trick: Er schaltet kurzerhand den Prozessor ab und kann dann auch die Zugriffszeiten des Prozessors für sich nutzen. Dies passiert jede achte Zeile (und bei Sprites, aber die ignorieren wir hier erst einmal).

Solche Zeilen werden Badlines genannt, da es bei einigen Rastertricks ungemein stört, wenn der Prozessor ausgeschaltet ist. Für die Synchronisation sind die Badlines aber hilfreich. Der Trick besteht im Wesentlichen darin, eine solche Badline künstlich herbeizuführen. Dadurch wird der Prozessor abgeschaltet und im 55sten Zyklus der Zeile wieder angeschaltet - der Prozessor ist synchronisiert.

Um eine Badline künstlich herbeizuführen, muss man nur einmalig (in einem der Zyklen 14 bis 54) in der Zeile einen sogenannten Badline-Zustand herbeiführen und das ist gar nicht so schwer: YSCROLL muss dafür einfach den letzten drei Bits der Rasterzeilennummer entsprechen. Das folgende Programm demonstriert dies:

*=$c000

         sei            ; Interrupt abschalten, der stört nur

loop     lda #111       ; Auf Zeile 111 warten
--       cmp $d012
         bne --

         and #$07       ; Letzte drei Bits der Rasterzeile
         ora #$18       ; plus die vorderen fünf Bits von $D011
         ldy $d011
         sta $d011
         sty $d011

         nop            ; Warten, damit man was sieht
         nop
         nop
         nop
         nop
         nop
         nop
         nop

         lda #$01
         ldy #$00

         sta $D021      ; Hintergrund weiß...
         sty $D021      ; ...und gleich wieder schwarz

         beq loop       ; Von Vorne beginnen

Die Änderung von YSCROLL sorgt zwar erfolgreich dafür, dass ein Badline-Zustand entsteht, verschiebt aber den Bildschirm in Y-Richtung. Das ist unschön, weshalb wir uns den vorigen Wert im Y-Register merken und direkt, nachdem der Prozessor aus der durch den Badline-Zustand ausgelösten Abschaltung wieder erwacht, ändern wir YSCROLL zurück.

Das war es auch schon. Der Befehl sty $d011 wird immer zum Zeitpunkt 55 der Rasterzeile ausgeführt (das ist genau bei den letzten 8 Pixeln vor dem rechten Rahmen) und von da an kann man einfach Taktzyklen zählen, um herauszufinden, an welcher Stelle sich der Rasterstrahl gerade befindet, wenn man ein Register ändert.

d011 sync4 schema.png

Eine Unschönheit fällt allerdings auf: In der Zeile befinden sich drei graue Blöcke. Wo kommen die her?

Jedesmal, wenn der VIC den Prozessor abschaltet, benötigt das drei Taktzyklen Vorlauf. Der Prozessor kann nämlich nicht abgeschaltet werden, wenn er gerade in den Speicher schreibt. Und der Prozessor kann halt in bis zu drei aufeinanderfolgenden Taktzyklen schreiben. Normalerweise ist das kein Problem, der VIC kümmert sich darum, dass das Abschalten zeitig genug passiert.

Wenn wir allerdings, wie hier, den Badline-Zustand künstlich herbeiführen, ist diese Zeit nicht mehr da. Der VIC schaltet zwar den Prozessor ab. Beginnt aber sofort, und nicht erst nach drei Taktzyklen, damit, aus dem Speicher zu lesen. Das klappt aber nicht, denn der Prozessor hat noch die Kontrolle über den Systembus, washalb der Lesezugriff des VIC geblockt wird. Der liest in den drei Takten deswegen immer das Zeichen 255. Und das ist auch das, was er da ausgibt; allerdings nur noch den unteren Teil, denn oberhalb war der Rasterstrahl ja schon.

Gleichzeitig mit dem Zeichen liest der VIC auch die Farbe des Zeichens. Dieser Lesezugriff wird nicht geblockt, allerdings bestimmt diesmal der Prozessor (und nicht der VIC), wo gelesen wird, nämlich beim Opcode des nächsten Befehls (also STY, was den Opcode 8C hat). Für die Farbe sind nur die untersten vier Bit relevant, also $C. Das ist aber der Farbcode für Grau. Deswegen sind die drei Blöcke grau.

Da in der Zeile nichts steht, könnte man die drei Blöcke etwas kaschieren, indem man sie schwarz färbt. Das geht. Dazu muss nur der Befehl nach dem sta einen Opcode haben, der auf $0 endet. Die Branch-Befehle bieten sich hier an. Wenn man also den Trick durch folgenden Code ersetzt, erhält man drei schwarze Blöcke:

         and #$07
         ora #$18
         ldx $d011
         sta $d011
         beq +
+        stx $d011

Durch die zusätzlichen Verzweigung sind allerdings zwei Taktzyklen hinzugekommen, die sollte man bei den nachfolgenden NOPs wieder abziehen, um die selbe Stelle für den Strich zu erhalten. Nur: Der Strich wird weiterhin von dem jetzt schwarzen Zeichen überlagert und es entsteht eine Lücke im Strich.

Vierter Versuch (einmaliger Badline-Zustand)[Bearbeiten | Quelltext bearbeiten]

Ausgabe des nebenstehenden Programms. Jetzt funktioniert es.

Je nachdem, was man mit dem exakten Timing anstellen will, kann es genügen, die drei Blöcke zu kaschieren. Allerdings muss man dabei berücksichtigen, dass sich die Position der drei Blöcke bei jedem Durchlauf ändern kann. Das hängt davon ab, was noch alles an Code kommt, bevor der Rücksprung zu loop erfolgt.

Es ist aber möglich, die drei Blöcke nahezu vollständig zu eliminieren. Lediglich beim aller ersten Aufruf, treten sie einmal (als kurzes Flackern) auf, was kaum auffällt:

*=$c000

         sei           ; Interrupt abschalten

         lda #111      ; Auf Zeile 111 warten
-        cmp $d012
         bne -

         and #$07      ; Badlinezustand erzeugen
         ora #$18
         ldx $d011
         sta $d011
         beq +
+        stx $d011

         nop
         nop
         nop
         nop
loop     nop
         nop
         nop

         lda #$01
         ldy #$00

         sta $D021     ; weißen Strich ausgeben
         sty $D021

         ldx #14       ; lange warten...
         ldy #00
-        dey
         bne -
         dex
         bne -

         ldx #116      ; und noch etwas warten...
-        dex
         bne -

         beq loop

Der Anfang ist der selbe, wie beim vorigen Versuch. Diesmal erzeugen wir den Badline-Zustand aber nur einmalig. Danach haben wir ja die Synchronisation und können diese weiter beibehalten. Die beiden Schleifen am Ende zählen genau so weit, bis der Rasterstrahl wieder kurz vor dem weißen Strich ist. Danach werden noch drei NOPs durchgeführt und der weiße Strich wird erneut an die selbe Stelle geschrieben, wie vorher. Nur: Diesmal treten keine unerwünschten Blöcke mehr auf.

Der Nachteil ist allerdings, dass wir jetzt den Programmcode sehr exakt timen müssen. Wenn das komplexer wird, kann das ziemlich viel Arbeit werden...

Noch ein Hinweis zu den Schleifen: Während eines kompletten Bildschirmaufbaus (ohne Sprites) führt der Prozessor 18581 Taktzyklen aus: In jeder Zeile stehen 63 Taktzyken zur Verfügung und es gibt 312 Zeilen: 63*312=18656. Davon muss man jetzt für jede der 25 Badlines noch 43 Taktzyklen abziehen (40, an denen der Prozessor abgeschaltet ist, und je 3 die dem Abschalten vorangehen. Das ergibt 25*43=1075 Taktzyklen, die man von 18656 noch abziehen muss. Das Ergebnis ist 18581. Man beachte: Das funktioniert hier, weil keine Schreibzugriffe stattfinden. Mit Schreibzugriffen müsste man genau nachhalten, ob diese während der Abschaltphase vorkommen und dann entsprechend mehr Zyklen berechnen; oder aber diese so timen, dass diese nicht in die Abschaltphase fallen können.