Was ich bei der Erstellung des Emulators über den Arcade-Automaten Bomb Jack gelernt habe
Ich habe kürzlich einen kleinen Emulator für eine Bomb Jack-Maschine geschrieben, hauptsächlich um herauszufinden, wie sich diese ersten 8-Bit-Arcade-Maschinen im Design von 8-Bit-Heimcomputern unterscheiden.
Wie ich viel später erfuhr, war ein Treffen auf einer Sommermesse in meiner Heimatstadt mit Spielautomaten wie Bomb Jack einer dieser Momente, die mein Schicksal veränderten. An einem normalen Sommertag kehrte ich nach Hause zurück, nachdem ich meinen gesamten Vorrat an Münzen für Spielautomaten ausgegeben hatte, und mein Kopf war voller Blumen und Soundeffekte. Ich habe versucht zu verstehen, wie diese Spiele funktionieren. Und dann verbrachte ich bis zum Ende des Jahres meine ganze Zeit nach der Schule damit, ziemlich verblasste Kopien dieser Arcade-Spiele auf meinem Heimcomputer zu erstellen. Ich war wie ein Fan des Frachtkultes von den Inseln des Pazifischen Ozeans, der aus Stöcken einen amerikanischen Militärradiosender schaffen wollte.
Zuerst dachte ich über die Idee nach, einen
Pengo- Emulator zu entwickeln, weil mein jugendliches Gehirn von diesem Spiel viel mehr beeindruckt war als Bomb Jack (hier ist übrigens meine
Frachtkult-Version von Pengo ). Für Pengo-Arcade-Geräte müssten jedoch neue Chip-Emulatoren für Audio und Video entwickelt werden. Für Bomb Jack gab es bereits genügend Teile (Z80 als CPU und AY-3-8910 für Sound), sodass ich als erster Bomb Jack übernahm.
Darüber hinaus war Bomb Jack eine großartige Gelegenheit, meinem Z80-Emulator endlich NMI-Unterstützung (Non-Maskable Interrupt) hinzuzufügen. Keiner der zuvor emulierten Z80-basierten Computer verwendete NMI. Daher war es nicht sinnvoll, diese Funktion neu zu erstellen. Ich konnte den Betrieb immer noch nicht überprüfen.
Wenn Sie nicht wissen, was Bomb Jack ist, sah dieses Spiel so aus (nicht sicher, ob ich das richtige Seitenverhältnis gewählt habe):
Die Version des Emulators in WebAssembly finden Sie hier:
https://floooh.imtqy.com/tiny8bit/bombjack.htmlNachdem der Ladevorgang abgeschlossen ist und die Highscore-Tabelle angezeigt wird, drücken Sie
1 , um eine Münze fallen zu lassen, und drücken Sie dann die
Eingabetaste (oder eine andere Taste außer den Pfeilen und der Leertaste), um das Spiel zu starten.
Verwenden Sie im Spiel die
Pfeiltasten , um die Richtung zu ändern, und die
Leertaste, um zu springen. Drücken Sie in der Luft die
Leertaste , um den Sturz zu verlangsamen.
Der Quellcode ist hier:
https://github.com/floooh/chips-test/blob/master/examples/sokol/bombjack.cEs verwendet
Chip-Header zur Emulation des Z80 und AY-3-8910 sowie
Sokol-Header als plattformübergreifender Wrapper (zur Eingabe der Anwendung, zum Rendern, zur Eingabe und zum Sound).
Schritt 1: Forschung
"Forschung" ist ein zu großes Wort: Ich habe gerade Googles "Bombjack Arcade-Hardware-Spezifikationen" getroffen.
Im Vergleich zu den beliebten Heimcomputern der 80er Jahre (oder sogar den mysteriösen osteuropäischen Computern, die oft noch aktive Communities haben) gibt es im Internet nur sehr wenige Informationen über Bomb Jack.
Ich habe zwei sehr wichtige Informationen gefunden: den
Schaltplan der Maschine und natürlich den
Quellcode des MAME-Emulators .
Es gibt auch ein Projekt, das
Bomb Jack auf FGPA implementiert , aus dessen VHDL-Quellen ich Details herausfinden konnte, die nicht im Schaltplan enthalten sind.
Das Verständnis des MAME-Quellcodes wäre schwierig, da Arcade-Maschinenemulatoren normalerweise nur eine Reihe von Makros sind, die beschreiben, wie verschiedene Geräte interagieren, aber es
gibt nicht viel
Quellcode .
Trotzdem erwiesen sich die Makrobeschreibungen der Geräte und insbesondere die Kommentare immer noch als sehr nützlich, um den Betrieb der Hardware zu verstehen, und wo sie zu kryptisch wurden (zum Beispiel der
Teil zur Videodecodierung ), reichten Versuch und Irrtum aus detaillierte Untersuchung des Konzepts.
Hardware-Übersicht
Das Interessanteste an der Bomb Jack-Hardware ist, dass es sich tatsächlich um
zwei Computer handelt, die über ein elektrisches Band miteinander verbunden sind: Es gibt eine
Hauptplatine mit Z80-CPU und Videodecodierungsgeräten sowie eine separate
Soundkarte mit eigener Z80-CPU und drei (ja, drei!) Soundchips AY-3-8910.
Videodecodierungsgeräte sind nicht als integrierte Schaltung implementiert - es handelt sich nur um viele kleine Allzweckchips (ihre Schaltung benötigt 6 von 10 Seiten des Schaltplans des Geräts). Beim Erstellen eines Emulators habe ich mich für einen kurzen Weg entschieden: Anstatt einzelne Teile des Videodecodierungsgeräts zu emulieren, habe ich nur dessen Verhalten emuliert, die entsprechende Ausgabe aus den Eingabedaten erstellt und mich nicht wirklich darum gekümmert, wie das Gerät selbst in der Mitte funktioniert.
Eine solche vereinfachte Lösung eignet sich durchaus für einen separaten Arcade-Automaten, der nur ein Programm ausführen kann. Wenn das Spiel startet und korrekt funktioniert, kann die Emulation als "gut genug" angesehen werden.
Darüber hinaus ist dieser vereinfachte Ansatz ein wichtiger Unterschied zur Emulation der meisten Heimcomputer: Einige Spiele erfordern eine genauere Emulation als andere. Beispielsweise benötigen Maschinen wie C64 oder Amstrad CPC eine sehr genaue Emulation bis zu Taktzyklen, sodass Videosysteme einiger Spiele und Grafiken Demos funktionierten korrekt.
Dies bedeutet auch, dass meine vorgefertigten CPU- und Soundchip-Emulatoren für Bomb Jack eigentlich überflüssig sind. Beispielsweise ist die Arbeit mit Z80-CPUs mit der Implementierung der Fraktalität des Maschinenzyklus übertrieben, eine einfachere und schnellere Fragmentierung auf Befehlsebene würde ausreichen.
Hauptplatine
Normalerweise versuche ich beim Schreiben eines neuen Emulators als erstes das Speicherzuweisungsschema herauszufinden (wo sich die Bereiche ROM und RAM, Videospeicher und spezielle Adressen oder Eingabe- / Ausgabeports befinden).
Es gibt nur einen „interessanten“ Chip auf der Hauptplatine von Bomb Jack - die Z80-CPU, die mit 4 MHz arbeitet. Der gesamte verbleibende Speicherplatz auf der Hauptplatine wird von Videodecodierungsgeräten belegt (mit Ausnahme eines Paares RAM- und ROM-Chips).
Der 16-Bit-Adressraum lautet wie folgt:
- 0000..7FFF : 32 KB ROM
- 8000..8FFF : 4 KB Allzweck-RAM
- 9000..93FF : 1 KB Videospeicher
- 9400..97FF : 1 KB Farb-RAM
- 9820..987F : 96 Byte Sprite-RAM
- 9C00..9CFF : 256 Byte RAM-Farbpalette
- 9E00, B000..B005, B800 : Eingabe-Ausgabe-Ports
- C000..DFFF : 8 KB ROM
Der E / A-Portbereich ist wie folgt. Einige Ports sind schreibgeschützt, andere schreibgeschützt und andere haben unterschiedliche Funktionen beim Lesen und Schreiben:
- 9E00 : schreiben: aktuelle Hintergrundbildnummer, lesen: -
- B000 : Lesen: Status des Joysticks von Spieler 1, Schreiben: Aktivieren / Deaktivieren der NMI-Maske
- B001 : Lesen: Status des Joysticks von Spieler 2, Schreiben: -
- B002 : Lesen: Münzen und Starttasten , Schreiben: -
- B003 : lesen: CPU-Watchdog, schreiben: ???
- B004 : Lesen: DIP-Schalter 1, Schreiben: Schalterbildschirm
- B005 : Lesen: DIP-Schalter 2, Schreiben: -
- B800 : Schreiben: Befehlssoundkarte, Lesen: -
Folgendes ist hier erwähnenswert:
- Das Gerät verfügt über viel ROM (40 KByte) und sehr wenig RAM (ca. 7 KByte, und nur 4 KByte davon sind "Allzweck-RAM").
- Für den RAM der Anzeige sind nur 2 KByte zugeordnet, aufgeteilt in zwei Fragmente von 1 KByte, was für eine 256 x 256-Vollfarbanzeige, bei der die Farben anscheinend Pixel für Pixel festgelegt werden, sehr klein erscheint
- Dies ist ein E / A-System in einem Speicherzuweisungsschema!
Die E / A im Speicherzuweisungsschema ist für eine Z80-Maschine etwas ungewöhnlich, da eines der Kennzeichen der Z80 der separate 16-Bit-Adressraum für E / A ist. Dies geschieht, um wertvollen Speicheradressraum zu sparen. Die E / A in einem Speicherzuweisungsschema befindet sich normalerweise in Computern mit einem 6502-Prozessor.
Ein Blick auf den Schaltplan bestätigt dies: Der IORQ-Pin wird auf der CPU der Hauptplatine nicht erkannt, nur der MREQ-Pin ist angeschlossen (der zum Initialisieren des Lesens oder Schreibens in den Speicher verwendet wird):
Dies bedeutet, dass wir uns nicht um E / A-Anforderungen für die CPU-Timer-Funktion der Hauptplatine im Emulator kümmern müssen, sondern nur um Speicheranforderungen.
Nachdem ich den Schaltplan studiert hatte, fand ich ein weiteres interessantes Detail über die CPU der Hauptplatine:
Es ist nur der NMI-Pin angeschlossen, während der INT-Pin immer einen hohen Taktpegel beibehält / inaktiv bleibt (dies bedeutet, dass die "normalen" maskierten Interrupts nicht ausgeführt werden und nur nicht maskierte Interrupts auftreten):
Dies ist auch für ein Auto mit dem Z80 recht ungewöhnlich. Bei allen Z80-basierten Heimcomputern, mit denen ich früher zu tun hatte, war das Gegenteil der Fall - sie verwendeten nur maskierbare Interrupts und niemals nicht maskierbare. Der maskierte Interrupt Z80 ist eine sehr flexible und ernsthafte Verbesserung im Vergleich zum primitiven Interrupt-System seines „unehelichen Vaters“ - Intel 8080 oder seines Konkurrenten - MOS 6502. Diese erhöhte Flexibilität ist jedoch auch schwieriger in Geräten zu implementieren (es sei denn, es handelt sich um eine Unterbrechungsquelle Es werden andere Chips der Z80-Familie verwendet, die bereits über ein integriertes komplexes Interrupt-Protokoll verfügen, wenn sie über einen Bus verbunden sind.
Na ja, genug Details über die Ausrüstung, gehen wir weiter zum Emulator!
Startvorgang
Der nächste Schritt nach dem Bestimmen der Speicherkonfiguration besteht darin, die emulierte CPU mit dem zugewiesenen Speicherzuordnungsschema zu verbinden, eine Art Visualisierung des Inhalts des Videospeichers aufzuzeichnen und die CPU-Zyklen zu starten.
Überraschenderweise reicht ein derart grober Ansatz oft aus, um den Ladevorgang zu durchlaufen und
etwas auf dem Bildschirm anzuzeigen. Beim Entwerfen des Bomb Jack-Emulators habe ich nur den Inhalt des 1-KB-Videospeichers im Bereich von 0x9000 bis 0x93FF als 32x32-Byte-Matrix verwendet. Wenn das Byte 0 war, habe ich einen Block schwarzer Pixel 8x8 gerendert, ansonsten einen Block weißer Pixel.
Dann habe ich einfach die emulierte CPU ausgeführt und auf das Beste gehofft. Siehe! Es erschien ein lesbares Bild:
Das obere Bild sieht beim Booten wie ein Hardware-Testbildschirm aus, und das untere sieht aus wie ein Score-Record-Bildschirm, der nach Abschluss des Startvorgangs angezeigt wird:
... aber um 90 Grad gedreht (was logisch ist, da sich der Bildschirm von Arcade-Automaten oft in einer vertikalen "Hochformat" -Orientierung befand).
Großartig, der Anfang ist vielversprechend!
Der nächste Schritt besteht darin, herauszufinden, wie diese weißen Blöcke in Farbpixel umgewandelt werden können ... (und dies ist ein großer Schritt. Details werden unten im Abschnitt zur Videodecodierung beschrieben).
Zuerst ging alles ziemlich schnell, auf dem Testbildschirm wurden Pixel und Farben beim Laden angezeigt (später bemerkte ich, dass die Farbdecodierung völlig falsch war und doch ...):
Aber als der Aufnahmebildschirm erscheinen sollte, bekam ich einen schwarzen Bildschirm. Als ich die Hintergrundfarbe so hackte, dass sie „nicht schwarz“ ist, stellte ich fest, dass die Pixel gerendert werden, aber die gesamte Farbpalette schwarz ist. Hmm ...
Nachdem ich ein paar Minuten auf diesen Bildschirm geschaut hatte, fiel mir ein, dass einige der Farben auf dem Highscore-Bildschirm animiert sind und wenn es eine Animation gibt, sollte es eine Art Timer geben. Die logische Zeitquelle in dieser Gerätekonfiguration ist das VSYNC-Anzeigesignal, und VSYNC ist mit dem NMI-Pin der CPU verbunden (oder besser gesagt nicht mit VSYNC, sondern mit VBLANK, dem kurzen Moment zwischen dem VSYNC-Signal und dem Kathodenstrahl, der sich in die obere linke Ecke bewegt).
Und das alles habe ich noch nicht umgesetzt ...
Als ich am nächsten Abend die erste Version der NMI-Verarbeitung zur Z80-Emulation hinzufügte und sie mit dem ersten Zähler vsync / vblank in der CPU-Timer-Funktion der Hauptplatine verband, begannen plötzlich viele Dinge zu passieren!
Zunächst wurden Farben auf dem Bildschirm der Aufzeichnungen angezeigt und einige davon wurden animiert:
Nach ein paar Sekunden begann etwas noch Aufregenderes! Der Highscore verschwand und eine seltsame Visualisierung der ersten Karte wurde angezeigt. Es war klar, dass dies ein Demo-Modus eines Arcade-Automaten ist, um Aufmerksamkeit zu erregen. Ich sah mehrere Bomben mit Farbanimationen, die verschwanden, als ein imaginärer Bomb Jack auf eine Karte sprang und diese Bomben sammelte:
Die Farben waren immer noch völlig falsch und doch ist es PROGRESS!
Es ist der richtige Zeitpunkt, um den Rest der Videodekodierung durchzuführen:
Video Bügeleisen
Auf den ersten Blick sah das Videoverarbeitungsgerät in Bomb Jack für eine 8-Bit-Maschine aus dem Jahr 1984 sehr leistungsfähig aus: Trotz der Auflösung von nur 256 x 256 Pixel konnte es gleichzeitig 128 (von 4096) Farben anzeigen und bis zu 24 Hardware-Sprites (16 x 16 Pixel) rendern. oder 32x32) mit Pixel für Pixel Farbe.
8-Bit-Heimcomputer hatten zu dieser Zeit ungefähr die gleiche Bildschirmauflösung, aber viele Farbbeschränkungen. Diese Einschränkungen werden beim Vergleich der Bomb Jack-Versionen für den ZX Spectrum- und Amstrad-CPC mit der Version für den Arcade-Automaten sehr deutlich:
Die
Version für das ZX Spectrum hatte eine ziemlich gute Pixelauflösung (256 x 192), aber nur sehr wenige Farben und litt unter dem typischen „Farbkonflikt“ -Effekt von Spectrum (obwohl die Entwickler sich sehr bemühten, damit es nicht zu auffiel):
Die Version für Amstrad CPC ist farbiger, aber um mehr Farben zu erhalten, mussten die Entwickler in den Anzeigemodus mit niedriger Auflösung (160 x 200) wechseln. Infolgedessen verwandelten sich Jack und die Monster in unleserliche Pixel:
Vergleichen Sie dies mit der Version für den Arcade-Automaten, die dieselbe Pixelauflösung wie das ZX Spectrum hatte, jedoch viel mehr Farben
und eine pixelweise Farbauflösung aufwies:
Interessant ist hier, dass die Arcade-Version bessere Grafiken hat, nicht weil sie auf leistungsfähigerer Hardware funktioniert (sie hat mehr ROMs zum Speichern von mehr Grafikdaten, aber die "Rechenleistung" ist ungefähr gleich), sondern weil sich die Entwickler des Geräts konzentrieren könnten über die Herstellung einer speziellen Maschine für eine bestimmte Art von Spiel und sie mussten keinen universellen Heimcomputer für den allgemeinen Gebrauch erstellen.
So funktioniert die Display-Hardware (zumindest in meiner High-Level-Interpretation):
Drei Anzeigeebenen
Das fertige Bomb Jack-Videosignal wird aus drei Ebenen kombiniert: einer Hintergrundebene, einer Frontebene und einer Sprite-Ebene.
Ein solches Schichtsystem hat zwei Hauptvorteile:
- Es implementiert eine ziemlich knifflige Hardware-Bildkomprimierung, um aus einer sehr kleinen Datenmenge ein Vollfarbenbild mit hoher Auflösung zu erzeugen.
- Dies reduziert den CPU-Aufwand für die Aktualisierung dynamischer Bildschirmelemente erheblich (selbst bei einer Frequenz von 4 MHz verfügt eine 8-Bit-CPU nicht über genügend Leistung, um so viele Objekte auf einem 256 x 256-Display mit einer Frequenz von 60 Hz zu bewegen).
Das Video-Bügeleisen unterscheidet sich erheblich von dem, was ich bei 8-Bit-Heimcomputern gesehen habe, aber MAME implementiert allgemeine Hilfsklassen für diese Art von Ausrüstung, sodass ich davon ausgehen kann, dass es bei Arcade-Automaten durchaus üblich ist.
Hintergrundschicht
Die Hintergrundebene kann 1 von 5 im ROM eingebetteten Hintergrundbildern rendern. Das Hintergrundbild wird ausgewählt, indem ein Wert von 1 bis 5 an die Adresse 0x9E00 geschrieben wird (es scheint, dass der Wert 0 etwas Besonderes ist und einen vollständig schwarzen Hintergrund ergibt).
Tatsächlich scheint das Gerät in der Lage zu sein, 7 verschiedene Bilder zu rendern, aber das Spiel verwendet nur 5. Insgeheim hatte ich gehofft, zuvor unentdeckte Bilddaten im ROM zu finden. Aber leider sind sie nicht da (ja, wahrscheinlich bin ich nicht der erste, der sie dort sucht).
So sieht die Hintergrundebene der ersten Karte ohne die beiden anderen Ebenen aus:
Die Hintergrundebene besteht aus
16 x
16 Pixel großen Kacheln.
Der Vorteil des Erstellens von Hintergrundbildern aus Kacheln besteht darin, dass dieselben Kacheln mehrmals verwendet werden können, sodass weniger Daten im ROM gespeichert werden können. Beachten Sie, dass der blaue Himmel, Teile der Pyramide und Sand unter der Pyramide dieselben Kacheln verwenden:
Um Speicherplatz zu sparen, implementiert die Hintergrundschichtausrüstung einen weiteren Trick - Kacheln können horizontal gedreht werden. Ich habe dies in meiner Implementierung fast übersehen, weil ich angenommen habe, dass die Software diese Hardwarefunktion nicht verwendet, aber einen kleinen Fehler im Hintergrund der dritten Karte festgestellt habe:
Ich habe den gleichen Trick auf der fünften Karte angewendet, aber hier ist es etwas schwieriger zu bemerken, wenn Sie nicht wissen, wonach Sie suchen sollen:
Vorderschicht:
Über der Hintergrundebene befindet sich die „vordere Ebene“, die alle festen Teile des Bildschirms darstellt, die dennoch von der CPU aktualisiert werden müssen (hauptsächlich Text, Plattformen und Bomben). Das Layout wird aus dem RAM gelesen (aus Fragmenten von 1-KB-RAM und 1-KB-Farb-RAM).
So sieht die isolierte vordere Ebene der ersten Karte aus:
Die vordere Ebene besteht ebenfalls aus Kacheln (sowie dem Hintergrund), verwendet jedoch kleinere 8x8-Kacheln:
Der Hauptvorteil der Aufteilung von Hintergrund und Front in separate Ebenen besteht darin, dass sich die CPU beim Erstellen oder Löschen von Frontelementen nicht um das Speichern und Wiederherstellen von Hintergrundpixeln kümmern muss.
Sprite-Schicht
Schließlich werden Hardware-Sprites über die vordere Ebene gerendert. Alles, was sich auf dem Bildschirm bewegt, wird in Sprites implementiert. Bomb Jack-Geräte können bis zu 24 Sprites rendern, und jedes Sprite kann eine Größe von 16 x 16 oder 32 x 32 Pixel haben. In diesem Fall können Sprites pixelgenau positioniert werden:
8x8 Fliesendecoder
Das Herzstück der Videodecodierungsausrüstung ist eine Farbpalette mit 128 Elementen und einem Kacheldecoder mit 8 x 8 Pixeln. Die Aufgabe des Kacheldecoders besteht darin, einen 7-Bit-Farbpalettenindex für jedes der 64 Pixel der Kachel zu erzeugen.
Diese 8x8-Kacheln sind die Bausteine für alles auf dem Bildschirm - 16x16-Hintergrundkacheln, 8x8-Kacheln für die vordere Ebene und 16x16- oder 32x32-Hardware-Sprites.Hier ist ein Blockdiagramm dieses 8x8-Kacheldecoders zum Rendern der vorderen Ebene (wie ich es verstanden habe):Erklärung des Top-Down-Blockdiagramms:- Der Decodierungsprozess beginnt oben mit dem Lesen des Bytes des „Kachelcodes“ aus dem Videospeicher (organisiert als Matrix aus 32x32-Kachelcodes) und eines separaten Bytes aus dem Farb-RAM (ebenfalls eine 32x32-Matrix). Das Abrufen der Codes für Kacheln und Farben aus dem Videospeicher erfolgt nur für die vordere Ebene. Ich habe sie jedoch hinzugefügt, um das Bild insgesamt verständlicher zu machen. Der 8x8-Kacheldecoder selbst benötigt nur einen Kachel- und Farbcode am Eingang.
- . ( ). , , ( ).
- 8 , 8 ( ). , , 8x8 24 (3 ).
- 64 7- . 3 , 4 — . , , 16 «», 8 . 8 .
- 7- , , 12- RGB- (4 ). ( , , ; , ).
Dies ist ein allgemeines Kacheldecodierungsschema, das von jeder der drei Anzeigeebenen verwendet wird, aber die Decodierung jeder Ebene unterscheidet sich geringfügig:- Die vordere Ebene kann tatsächlich 512 verschiedene 8x8-Kacheln rendern. Dies erfordert 9-Bit-Kachelcodes, aber der Videospeicher bietet nur 8 Bit pro Kachel. Das neunte Bit wird aus dem fünften Bit des Farbwerts "entlehnt" (da nur 4 Bits des Farbwerts zum Erstellen des Farbpalettenindex verwendet werden, verbleiben 4 weitere Bits für andere Zwecke). Wenn alle 3 Bits aus den 8x8-Kachelbitschichten gleich Null sind, wird das vordere Pixel als transparent betrachtet und das Hintergrundpixel "durchscheint" es.
- 16x16, 16x16=256 256 (512 ). , 16x16 8x8, . , ; «» : 7 , .
- 16x16 32x32 , 4 16 8x8 . , 16x16 96 , 32x32 — 384 . , 3 , .
Um besser zu verstehen, wie Kachelbitebenen aussehen, habe ich ein kleines C-Programm geschrieben , das ROM-Kacheln in PNG-Dateien konvertiert (3 Bits pro Pixel, konvertiert in 8 Graustufenebenen).Das Folgende zeigt die ROM-Kacheln der vorderen Schicht. Wir sehen Zahlen und Textschriftdaten, Plattformkacheln, Bomben (in zwei Hälften geteilt), Teile des Logos des Bomb Jack-Bildschirmschoners und die Anzahl der Punkte-Multiplikatoren, die oben auf dem Bildschirm angezeigt werden (übrigens ist alles um 90 Grad gedreht, da auch der gesamte Bildschirm gedreht ist ):Betrachten Sie als nächstes die ROM-Kacheln des Hintergrunds. Es sieht nicht sehr klar aus, denn wir beobachten tatsächlich, dass 16x16-Kacheln in 8x8-Kacheln dekodiert werden. Jede 16x16-Kachel wird aus vier benachbarten 8x8-Kacheln erstellt. Aber Sie können Teile des griechischen Tempels von Karte 2, die Burg von Karte 3 und Wolkenkratzer von Karte 4 erkennen.Und schließlich ROM-Sprite-Kacheln. 16x16 Sprites belegen die obere Hälfte und 32x32 Sprites die untere Hälfte.Ein interessanter Hack des Bomb Jack-Bildschirmschoners ist, dass das Logo aus Frontkacheln und Sprites zusammengesetzt ist. Ich denke, dass den Entwicklern die vorderen Kachel-ROMs ausgegangen sind, aber im Sprite-ROM war nur noch wenig Platz:Sprite-Ausrüstung
Die Sprite-Ausrüstung von Bomb Jack ist sehr leistungsfähig im Vergleich zu den damaligen Heimcomputern:- Es können bis zu 24 Hardware-Sprites gerendert werden. Es scheint, dass es keine Einschränkungen hinsichtlich der Anzahl der Sprites pro Scanzeile gab.
- Sprites können eine Größe von 16 x 16 Pixel oder 32 x 32 Pixel haben
- Jedes Sprite kann einen von 16 Slots mit 8 Farben in einer gemeinsamen Farbpalette auswählen
- Sprites hatten eine Pixel-für-Pixel-Farbauflösung.
- Jedes Sprite kann vertikal oder horizontal gespiegelt werden
- Jedes Sprite kann eines von 128 Sprite-Bildern auswählen, die im ROM geflasht wurden.
Beim Dekodieren von Pixeln und Sprites eines Sprite-Systems wird dieselbe 8x8-Basiskachel verwendet wie in den Hintergrund- und Frontebenen.Sprite-Attribute befinden sich im Adressbereich von 0x9820 bis 0x987F - 96 Byte, 4 Byte pro Sprite. Soweit ich gesehen habe, dient dieser Bereich nur zur Aufnahme; Zumindest führt die CPU keinen Lesezugriff auf diesen Speicherbereich durch.Jedes Sprite wird durch 4 Bytes beschrieben:- Byte 0 :
- Bit 7 : Wenn gesetzt, ist dies ein 32x32-Sprite, andernfalls 16x16
- Bits 6..0 : 7 Bits zum Festlegen des Codes der Sprite-Kachel, die zum Suchen nach Bitebenen des Sprite-Bilds in den ROM-Kacheln verwendet wird.
- Byte 1 :
- Bit 7 : Wenn gesetzt, wird das Sprite horizontal gespiegelt
- Bit 6 : Wenn gesetzt, wird das Sprite vertikal gespiegelt
- Bits 3..0 : 4 Bits zum Einstellen des Farbwerts für den Kacheldecoder
- Byte 2 : Sprite-Position der X-Achse auf dem Bildschirm
- Byte 3 : Position des Sprites auf dem Bildschirm entlang der Y-Achse
Es ist nicht klar, was die Bits 4 und 5 von Byte 1 tun, der Kommentar in MAME sagt dies:
e ? (, )
f ? (, (B)?)
Speicher-E / A-Ports
Einige Hinweise zu den Eingangs- / Ausgangsanschlüssen der Hauptplatine. Wie oben erwähnt, sehen die E / A-Ports folgendermaßen aus:
- 9E00 : schreiben: aktuelle Hintergrundbildnummer, lesen: -
- B000 : Lesen: Status des Joysticks von Spieler 1, Schreiben: Aktivieren / Deaktivieren der NMI-Maske
- B001 : Lesen: Status des Joysticks von Spieler 2, Schreiben: -
- B002 : Lesen: Münzen und Starttasten , Schreiben: -
- B003 : lesen: CPU-Watchdog, schreiben: ???
- B004 : Lesen: DIP-Schalter 1, Schreiben: Schalterbildschirm
- B005 : Lesen: DIP-Schalter 2, Schreiben: -
- B800 : Schreiben: Befehlssoundkarte, Lesen: -
Die Adresse 0x9E00 (Auswahl des Hintergrundbilds), die wir oben bereits berücksichtigt haben, und die Adresse 0xB800 (Befehlssoundkarte) werden wir im nächsten Abschnitt berücksichtigen. Bleibt die Adressen von 0xB000 bis 0xB005:
Das Lesen von den Adressen 0xB000 und 0xB001 gibt den aktuellen Status der beiden Joysticks zurück. Festgelegte Bytes zeigen geschlossene Joystick-Schalter an:
- Bit 0 : richtige Richtung
- Bit 1 : linke Richtung
- Bit 2 : Aufwärtsrichtung
- Bit 3 : Abwärtsrichtung
- Bit 4 : Sprungtaste gedrückt
Die restlichen 3 Bits werden ignoriert.
Das Lesen von 0xB002 gibt den Status des Münzprüfers und der Starttasten zurück:
- Bit 0 : Spieler 1 Münze wird geworfen
- Bit 1 : Spieler 2 Münze wird geworfen
- Bit 2 : Starttaste für Spieler 1
- Bit 3 : Startknopf für Spieler 2
Das Lesen von den Adressen 0xB004 und 0xB005 gibt den Status der Dip-Schalter zurück, mit denen das Verhalten des Arcade-Automaten konfiguriert wird:
- B004 :
- Bits 0,1 : Wie viele „Spiele“ gibt es für eine Münze (1, 2, 3 oder 5)?
- Bits 2,3 : Gleiches gilt für Spieler 2
- Bits 4,5 : Wie viele Leben pro Spiel (3, 4, 5 oder 2)
- Bit 6 : Die Position des Arcade-Automaten: "Cocktail-Tisch" oder "vertikal".
- Bit 7 : Gibt an, ob im Standby-Modus Ton abgespielt werden soll
- B005 :
- Bits 3.4 : Schwierigkeitsgrad 1 (Vogelgeschwindigkeit)
- Bits 5,6 : Schwierigkeitsgrad 2 (Anzahl und Geschwindigkeit der Feinde)
- Bit 7 : Häufigkeit des Auftretens einer bestimmten Münze
Schließlich implementiert das Lesen von Adresse
B003 einen Software-Watchdog. Die CPU muss häufig von dieser Adresse lesen, sonst führt der Arcade-Computer einen Hardware-Reset durch. Wenn das Spiel aus irgendeinem Grund abstürzt, wird das Gerät automatisch neu gestartet.
Sie können an einige E / A-Portadressen schreiben:
- B000 : ob NMI während vblank generiert werden soll; scheint nur während des Startvorgangs deaktiviert zu sein
- B004 : den gesamten Bildschirm umdrehen; Ich habe die Verwendung dieser Funktion noch nie erlebt, aber ich habe eine Theorie darüber (siehe unten).
Die Funktion zum Umdrehen des Bildschirms ist etwas verwirrend, da ich beim Spielen eines Spiels nie gesehen habe, wie es verwendet wird. Ich habe jedoch eine Ahnung, was er tut, aber um dies zu bestätigen, müssen Sie Code schreiben. Wenn sich der Arcade-Automat in der Konfiguration „Cocktail Table“ befindet, sitzen sich zwei Spieler gegenüber. Daher schlug ich vor, dass diese Funktion den Bildschirm umdreht, wenn ein Spiel von Spieler 1 zu Spieler 2 wechselt. Ich habe den Zwei-Spieler-Modus jedoch noch nicht im Emulator implementiert.
Soundkarte
Die Soundkarte selbst ist ein voll ausgestatteter Computer mit einer Z80-CPU (mit einer Frequenz von 3 MHz), drei Soundchips (AY-38910 mit einer Frequenz von 1,5 MHz) sowie RAM und ROM. Das Speicherzuweisungsschema der Soundkarte sieht ziemlich einfach aus:
- 0000..2000 : 8 KB ROM
- 4000..4400 : 1 KB RAM
- 6000 : Soundbefehl von der Hauptplatine
Da das Speicherzuordnungsschema oberhalb der 0x8000-Adresse nichts Interessantes enthält, ist der oberste Adresskontakt der CPU nicht einmal verbunden:
Die spezielle Adresse 0x6000 ist der im Speicher befindliche E / A-Port (8-Bit-Latch), der nicht dem realen RAM entspricht. Dies ist derselbe Port, der sich auf der Hauptplatine bei 0xB800 befindet. Es ist ein Kommunikationskanal zwischen der Haupt- und der Soundkarte.
Die drei Soundchips werden von diesen Z80-Ausgabeanweisungen gesteuert, nicht über die Speicheranschlüsse. In AY-3-8910 sind nur zwei E / A-Ports geöffnet, der erste dient zum Speichern der Registernummer und der zweite zum Schreiben oder Lesen des Inhalts des vom ersten Port angegebenen Registers.
Die E / A-Schaltung ist wie folgt:
- 0x00 : erster Soundchip: Registerauswahl
- 0x01 : erster Soundchip: Zugriff auf das ausgewählte Register
- 0x10 : zweiter Soundchip: Registerauswahl
- 0x11 : zweiter Soundchip: Zugriff auf das ausgewählte Register
- 0x80 : dritter Soundchip: Registerauswahl
- 0x81 : dritter Soundchip: Zugriff auf das ausgewählte Register
Ein paar Worte zum Soundchip AY-3-8910:
Dies ist ein ziemlich Standardgerät, das in Heimcomputern dieser Zeit sehr beliebt ist (z. B. in Amstrad CPC, ZX Spectrum 128, in MSX-Computern und vielen anderen). AY-3-8910 brachte viele Variationen und Klone hervor (zum Beispiel Yamaha YM2149, das an sich die Grundlage für eine ganze Familie leistungsstärkerer Soundchips wurde).
AY-3-8910 verfügt über 3 Kanäle mit rechteckigen Signalen, einen Rauschgenerator, der mit drei Kanälen gemischt werden kann, und einen Hüllkurvengenerator. Da es für alle drei Kanäle nur einen Hüllkurvengenerator gab, war dies nicht besonders nützlich, und die meisten Spiele verwendeten eine CPU, um Ton und Lautstärke zu modulieren.
Dies bedeutet, dass der AY-3-8910-Chip mehr CPU-Eingriffe erfordert, um einen qualitativ hochwertigen Sound zu erzeugen (im Gegensatz zu mehr eigenständigen SID-Chips, beispielsweise in einem C64-Computer).
Es ist erstaunlich zu sehen, was mit drei ziemlich einfachen Soundchips und der CPU, die sie steuert, gemacht werden kann. Die Musik und Soundeffekte von Bomb Jack sind viel reicher als ich es in den meisten Heimcomputerspielen gehört habe.
Das einzige, was an dieser Soundkarte wirklich interessant ist, ist die Art und Weise, wie sie ihre Befehle von der Hauptplatine empfängt.
Sound Command Latch
Der „Sound Latch“ ist ein Einzelbyte-Speicher (8-Bit-Latch), der den Haupt- und Soundkarten gemeinsam ist. Der Latch ist an die Adresse 0xB800 auf der Hauptplatine und an die Adresse 0x6000 auf der Soundkarte gebunden.
Wenn der NMI-Interrupt mit VSYNC eingeschaltet wird, führt die Soundkarte eine sehr einfache Interrupt-Serviceroutine durch, die den Hardware-Latch liest, in die normale Speicheradresse schreibt und das „Signalbit“ setzt, das der „Hauptschleife“ mitteilt, dass ein neuer Soundbefehl empfangen wurde:
ex af,af' ;0066 exx ;0067 ld hl,04390h ;0068 set 0,(hl) ;006b ld a,(06000h) ;006d ld (04391h),a ;0070 exx ;0073 ex af,af' ;0074 retn ;0075
Die NMI-Kontaktaktivierungsmethode unterscheidet sich geringfügig von der Hauptplatinenmethode:
Auf der Hauptplatine wird der NMI-Pin für die Dauer des VBLANK-Laufs aktiv.
Auf der Soundkarte wird NMI jedoch aktiviert, wenn VSYNC ausgelöst wird, und bleibt nicht während der VBLANK aktiv, sondern bis die Interrupt-Service-Prozedur die Daten aus dem Latch bei 0x6000 liest.
Wenn das Gerät das Lesen von der Adresse 0x6000 erkennt, führt es zwei fest codierte Operationen aus:
- Der Inhalt des Soundclips wurde auf 0 zurückgesetzt
- Der NMI-Kontakt wird inaktiv
Tatsächlich ist dies eine einfache Eliminierung des Kontaktsprungs, bei der ein Soundbefehl nicht zweimal ausgeführt werden kann.
Die Frage bleibt nur: Wie oft schreibt die Hauptplatine einen neuen Befehl (da die Art und Weise, wie die Emulation von zwei Platinen implementiert wird, davon abhängt).
Nach dem Debuggen mit printf stellte ich fest, dass die Hauptplatine höchstens einen Soundbefehl pro 60-Hz-Frame aufzeichnet. Dies vereinfachte die Struktur des "Hauptzyklus" des Emulators erheblich.
Das Problem der gemeinsamen Arbeit zweier separater emulierter Computer, die Daten miteinander austauschen müssen, besteht darin, dass die Emulation eines Computers nur dann wirksam ist, wenn mehrere Zyklen gleichzeitig ohne Interferenz ausgeführt werden können.
Der schlimmste Fall wäre zum Beispiel:
- Wir führen eine Anweisung in Computer 1 aus
- Wir führen eine Anweisung in Computer 2 aus
- wiederholen ...
Mein Z80-Emulator ist nicht für das Beenden und Eingeben der Emulation für jeden Befehl optimiert, da er in diesem Fall in den Speicher spülen und den Status der CPU am Anfang und Ende jedes Befehls aus dem Speicher laden sollte. Wenn die CPU viele Befehle störungsfrei verarbeiten kann, können Sie den größten Teil des CPU-Zustands in den Registern speichern und den Zustand des letzten Befehls auf den Speicher zurücksetzen.
Das heißt, eine ideale Situation wäre folgende: Wir führen ein emuliertes System ohne Interferenz über den gesamten Frame des Host-Systems aus (für eine CPU mit einer Frequenz von 4 MHz und 60 Hz bedeutet dies ungefähr 67.000 Zyklen pro Frame oder irgendwo zwischen 3.000 und 16.000 Anleitung Z80).
Bei der Arbeit mit Bomb Jack musste ich sicherstellen, dass die Hauptplatine keinen neuen Befehl aufzeichnet, bevor die Soundkarte den letzten Befehl lesen kann. Bevor ich herausfand, dass die Hauptplatine nicht mehr als einen Befehl pro Frame aufzeichnet, überlegte ich, ob eine komplexe Befehlswarteschlange erstellt werden muss, die Aufzeichnungen im Sound-Latch der Hauptplatine abfängt und die Zyklusnummer und das Befehlsbyte in der Warteschlange speichert.
Zu dem Zeitpunkt, als die Soundkarte ihren Frame ausführte, nahm sie einen neuen Befehl aus der Befehlswarteschlange entgegen, wenn die Befehlszyklusnummer erreicht war.
Ein solches System würde funktionieren und „korrekt“ sein, aber die Komplexität des Codes erheblich erhöhen.
Am Ende entschied ich mich für eine viel einfachere Lösung ohne Warteschlangen. Da die Hauptplatine nur einen Befehl pro Frame aufzeichnet, habe ich die Ausführung auf zwei Computern abwechselnd ausgeführt, sodass jeder von ihnen zwei Zeitscheiben pro Frame ausführte:
- Führen Sie die erste Hälfte des Rahmens auf der Hauptplatine aus
- Führen Sie die erste Hälfte des Frames auf der Soundkarte aus
- Führen Sie die zweite Hälfte des Rahmens auf der Hauptplatine aus
- Führen Sie die zweite Hälfte des Frames auf der Soundkarte aus
Dies stellt sicher, dass die Soundkarte jeden von der Hauptplatine aufgezeichneten Befehl korrekt sieht und gleichzeitig jede Emulation für Tausende von Zyklen ausführen kann.
Die Tatsache, dass das Host-System mit einer Bildrate von 60 Hz arbeitet, ist natürlich eine sehr kühne Annahme :)
Und der letzte ...
Die letzte interessante Tatsache über die Emulatorversion in WebAssembly:
Komprimierte Größe aller heruntergeladenen Dateien beim Ausführen des Emulators in WebAssembly
ungefähr gleich 113 KByte:
- ca. 2,5 KB für HTML, CSS und "handgeschriebenes" JS
- 26,8 KB pro emscripten Laufzeit-JS-Datei
- 83,7 KB pro WASM-Datei
Die WASM-Datei enthält die integrierten ROMs des Arcade-Automaten.
Unkomprimiert belegen diese ROMs 112 KB.
Das heißt, der
gesamte komprimierte Emulator mit integrierten ROMs belegt fast das gleiche Volumen wie unkomprimierte ROMs :)
112-Kilobyte-ROMs werden auf ungefähr 57 KB komprimiert, dh die wahre Größe des komprimierten Codes in WASM ohne ROM-Daten beträgt weniger als 30 KB (84-57).
Es scheint mir ziemlich gut für einen vollständigen Emulator eines 8-Bit-Systems zu sein;)