NES / Famicom / Dandy-Emulation auf Webtechnologien. Yandex-Bericht

Mit dem TypeScript-, Canvas- und Web-Audio-Stack können Sie Computersysteme mithilfe von Webtechnologien emulieren. In meinem Bericht habe ich am Beispiel der NES-Set-Top-Box erklärt, wie die Architektur von Computern angeordnet ist - ein Prozessor, ein Programm, Peripheriegeräte, E / A-Zuordnung zum Speicher.





Der Bericht kann in drei Teile unterteilt werden:



  1. wie der 6502 Prozessor funktioniert und wie man ihn mit JavaScript emuliert,
  2. wie das Grafikausgabegerät funktioniert und wie Spiele ihre Ressourcen speichern,
  3. wie Sound mithilfe von Web-Audio synthetisiert wird und wie er mithilfe eines Audio-Orlets in zwei Streams parallelisiert wird.


Ich habe versucht, Tipps zur Optimierung zu geben. Trotzdem ist Emulation die Sache, bei 60 FPS bleibt wenig Zeit für die Codeausführung.



- Hallo allerseits, mein Name ist Zhenya. Jetzt wird es am Samstag an vielen Samstagen ein kleines ungewöhnliches Gespräch über das Projekt geben. Lassen Sie uns über die Emulation von Computersystemen sprechen, die zusätzlich zu vorhandenen Webtechnologien implementiert werden können. Tatsächlich ist das Web bereits ziemlich reich an Werkzeugen, und Sie können absolut erstaunliche Dinge tun. Insbesondere werden wir mit allen über den Emulator sprechen, wahrscheinlich mit der berühmten Dandy-Konsole aus den 90er Jahren, die eigentlich als Nintendo Entertainment System bezeichnet wird.







Erinnern wir uns an eine kleine Geschichte. Es begann 1983, als der Famicom in Japan herauskam. Es wurde von Nintendo veröffentlicht. 1985 wurde die amerikanische Version veröffentlicht, die als Nintendo Entertainment System bezeichnet wurde. In den 90er Jahren hatten wir dieselbe taiwanesische Region namens Dandy, aber insgeheim ist dies ein inoffizielles Präfix. Und das letzte derartige eiserne Geschenk von Nintendo war 2016, als der NES mini herauskam. Leider habe ich keinen NES mini. Es gibt SNES mini, Super Nintendo. Schauen Sie sich an, was für eine kleine Sache, und direkt auf dieser Folie können Sie Moores Gesetz in seiner ganzen Pracht sehen.



Wenn wir uns 1985 und das Verhältnis der Konsole zum Joystick ansehen und 2016 sehen wir, wie viel kleiner alles geworden ist, weil sich die Hände der Menschen nicht ändern, kann der Joystick nicht kleiner gemacht werden, aber die Konsole selbst ist winzig geworden.



Wie wir bereits bemerkt haben, gibt es viele Emulatoren. Wir haben es nicht gesagt, aber mindestens ein Beamter hat es bemerkt. Dieses Ding - SNES mini oder NES mini - ist keine echte Set-Top-Box. Dies ist eine Hardware, die die Konsole emuliert. Das heißt, dies ist in der Tat ein offizieller Emulator, der jedoch in einer so lustigen Eisenform vorliegt.



Aber wie wir wissen, gibt es seit den 2000er Jahren Programme, die das NES emulieren, dank derer wir immer noch Spiele aus dieser Zeit genießen können. Und es gibt viele Emulatoren. Warum fragst du mich noch einen, besonders in JavaScript? Als ich das tat, fand ich drei Antworten auf diese Frage für mich.



  1. , . - , . . , - , - . . . , , . , -.
  2. , , . , , , , NES — , , NTSC, 60 . 16 , . .
  3. . , . , , . , , — , . . , , .


Ich habe mir auch die Präsentation von Matt Godbold angesehen, der auch über die Emulation des Prozessors sprach, auf dem das NES ausgeführt wird. Er sagte, es sei lustig, dass wir eine so niedrige Sache in einer so hohen Sprache emulieren. Wir haben keinen Zugang zu Hardware, wir arbeiten indirekt.







Fahren wir mit der Überlegung fort, was wir emulieren, wie wir emulieren usw. Wir beginnen mit dem Prozessor. Das NES selbst ist eine Ikone. Für Russland ist es verständlich, dass dies ein kulturelles Phänomen ist. Aber im Westen und im Osten, in Japan, war es auch ein kulturelles Phänomen, weil die Konsole tatsächlich die gesamte Heimvideospielbranche rettete.



Der Prozessor ist auch im legendären MOS6502 installiert. Welche Bedeutung hat es? Zum Zeitpunkt des Erscheinens hatten die Konkurrenten einen Preis von 180 US-Dollar und der MOS6502 einen Preis von 25 US-Dollar. Das heißt, dieser Prozessor hat die PC-Revolution ausgelöst. Und hier habe ich zwei Computer. Das erste ist Apple II, wir alle wissen und stellen uns vor, wie wichtig dieses Ereignis für die Welt der PCs war.



Es gibt auch einen BBC Micro Computer. Er war in Großbritannien populärer, die BBC ist ein britischer Fernsehkonzern. Das heißt, dieser Prozessor brachte Computer in die Massen, dank ihm sind wir jetzt Programmierer, Front-End-Entwickler.



Werfen wir einen Blick auf das Mindestprogramm. Was brauchen wir, um ein Computersystem herzustellen?







Die CPU selbst ist ein ziemlich nutzloses Gerät. Wie wir wissen, führt die CPU das Programm aus. Aber zumindest damit dieses Programm irgendwo gespeichert werden kann, wird Speicher benötigt. Und natürlich ist es im Mindestprogramm enthalten. Und unser Speicher besteht aus Zellen mit acht Bits, die als Bytes bezeichnet werden.



In JavaScript können wir typisierte Uint8Array-Arrays verwenden, um diesen Speicher zu emulieren, dh wir können ein Array zuweisen.



Damit der Speicher mit dem Prozessor verbunden werden kann, gibt es einen Bus. Der Bus ermöglicht es dem Prozessor, den Speicher über Adressen zu adressieren. Adressen bestehen nicht mehr wie Daten aus acht Bits, sondern aus 16, wodurch wir 64 Kilobyte Speicher adressieren können.







Es gibt einen bestimmten Zustand im Prozessor, es gibt drei Register - A, X, Y. Ein Register ist wie ein Speicher für Zwischenwerte. Die Registergröße beträgt ein Byte oder acht Bits. Dies sagt uns, dass der Prozessor 8-Bit ist und mit 8-Bit-Daten arbeitet.



Ein Beispiel für die Verwendung des Registers. Wir möchten zwei Zahlen hinzufügen, aber es ist nur ein Bus im Speicher. Es stellt sich heraus, dass Sie die erste Nummer irgendwo dazwischen speichern müssen. Wir speichern es in Register A, wir können den zweiten Wert aus dem Speicher nehmen, ihn hinzufügen und das Ergebnis erneut in Register A ablegen.



Funktionell sind diese Register ziemlich unabhängig - sie können als allgemeine verwendet werden. Sie haben jedoch eine Bedeutung, z. B. Addition, das Ergebnis wird in Register A erhalten und der Wert des ersten Operanden wird genommen.



Oder wir adressieren zum Beispiel Daten. Wir werden etwas später darüber sprechen. Wir können den Offset-Adressierungsmodus angeben und das X-Register verwenden, um den endgültigen Wert zu erhalten.



Was ist noch im Prozessorstatus enthalten? Es gibt ein PC-Register, das auf die Adresse des aktuellen Befehls zeigt, da die Adresse zwei Bytes beträgt.



Wir haben auch das Statusregister, das die Statusflags anzeigt. Wenn wir beispielsweise zwei Werte subtrahieren und negativ werden, leuchtet ein bestimmtes Bit im Flag-Register.



Schließlich gibt es SP, einen Zeiger auf den Stapel. Der Stapel ist nur gewöhnlicher Speicher, er ist nicht von allem anderen, von allen anderen Programmen getrennt. Es gibt einfach eine Anweisung vom Prozessor, die diesen SP-Zeiger steuert. So wird der Stack implementiert. Dann schauen wir uns eine großartige Computeridee an, die zu solch interessanten Lösungen führt.







Jetzt wissen wir, dass der Prozessor einen Prozessor, einen Speicher und einen Status enthält. Mal sehen, was unser Programm ist. Dies ist eine Folge von Bytes. Es muss nicht einmal konsistent sein. Das Programm selbst kann sich in verschiedenen Teilen des Speichers befinden.



Wir können uns ein Programm vorstellen, ich habe hier einen Code - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. Dies ist ein echtes Programm für 6502. Jedes Byte dieses Programms, jede Ziffer in diesem Array ist Entität als Opcode. Opcode - Operationscode. „Dann wieder eine gewöhnliche Zahl.



Zum Beispiel gibt es einen Opcode 169. Er codiert zwei Dinge an sich - erstens eine Anweisung. Bei der Ausführung ändert der Befehl den Status des Prozessors, des Speichers usw., dh den Status des Systems. Zum Beispiel fügen wir zwei Zahlen hinzu, das Ergebnis erscheint in Register A. Dies ist eine Beispielanweisung. Wir haben auch eine LDA-Anweisung, die wir genauer betrachten werden. Es lädt einen Wert aus dem Speicher in Register A.



Das zweite, was der Opcode codiert, ist der Adressierungsmodus. Er gibt Anweisungen, wo sie ihre Daten bekommen kann. Wenn dies beispielsweise der IMM-Adressierungsmodus ist, heißt es: Nehmen Sie die Daten, die sich in der Zelle neben dem aktuellen Programmzähler befinden. Wir werden auch sehen, wie dieser Modus funktioniert und wie er in JavaScript implementiert werden kann.



So ist das Programm. Abgesehen von diesen Bytes ist alles JavaScript sehr ähnlich, nur auf einer niedrigeren Ebene.







Wenn Sie sich erinnern, worüber ich gesprochen habe, kann es ein lustiges Paradoxon geben. Es stellt sich heraus, dass wir das Programm und auch die Daten im Speicher speichern. Man könnte diese Frage stellen: Kann ein Programm als Daten fungieren? Die Antwort ist ja. Wir können zum Zeitpunkt der Ausführung dieses Programms vom Programm selbst wechseln.



Oder eine andere Frage: Können Daten ein Programm sein? Ja auch. Der Prozessor spielt keine Rolle. Er mahlt wie eine Mühle die ihm zugeführten Bytes und folgt den Anweisungen. Eine paradoxe Sache. Wenn Sie darüber nachdenken, ist es super unsicher. Sie können mit der Ausführung eines Programms beginnen, das nur Daten auf einem Stapel usw. enthält. Der Vorteil ist jedoch, dass es sehr einfach ist. Keine komplizierten Schaltungen erforderlich.



Dies ist die erste großartige Idee, die uns heute begegnet. Es heißt von Neumann Architektur. Aber es gab tatsächlich viele Mitautoren.



Hier ist dargestellt. Es gibt Programm 1, Opcode 169, gefolgt von 10, einige Daten. Okay. Dieses Programm kann auch folgendermaßen angezeigt werden: 169 sind Daten und 10 ist ein Opcode. Dies wird ein legales Programm für 6502 sein. Dieses gesamte Programm kann wiederum als Daten betrachtet werden.



Wenn wir einen Compiler haben, können wir etwas erstellen, es in dieses Gedächtnis einfügen, und es wird so eine lustige Sache sein.



Werfen wir einen Blick auf den ersten Teil unseres Programms - Anweisungen.







6502 bietet Zugriff auf 73 Anweisungen, einschließlich Arithmetik: Addition, Subtraktion. Keine Multiplikation und Division, sorry. Es gibt Bitoperationen, bei denen es darum geht, Bits in Acht-Bit-Wörtern zu manipulieren.



Es gibt Sprünge, die in unserem Frontend verboten sind: die Sprunganweisung, die einfach den Programmzähler auf einen Teil des Codes überträgt. Dies ist in der Programmierung verboten, aber wenn Sie mit niedriger Ebene arbeiten, ist dies die einzige Möglichkeit, eine Verzweigung durchzuführen. Es gibt Operationen für den Stapel usw. Sie sind gruppiert. Ja, wir haben 73 Anweisungen, aber wenn Sie sich die Gruppen ansehen und was sie tun, gibt es tatsächlich nicht so viele von ihnen und sie sind sich alle ziemlich ähnlich.



Kehren wir zur LDA-Anweisung zurück. Wie gesagt, dies ist "Laden Sie den Wert aus dem Speicher in Register A". So einfach kann es in JavaScript sein. Am Eingang befindet sich die Adresse, die uns der Adressierungsmodus liefert. Wir ändern den Zustand im Inneren, wir sagen, dass this._a gleich dem gelesenen Wert aus dem Speicher ist.



Wir müssen diese beiden Bitfelder noch im Statusregister setzen - ein Null-Flag und ein negatives Flag. Hier gibt es eine Menge bitweises Zeug. Wenn Sie jedoch einen Emulator erstellen, wird es für Sie zur zweiten Natur, sich mit diesen OPs, Negativen usw. zu befassen. Das einzig lustige daran ist, dass es im zweiten Zweig solche% 256 gibt. Es verweist uns erneut auf die Natur unserer geliebten JavaScript-Sprache, auf die Tatsache, dass sie keine typisierten Werte hat. Der Wert, den wir in Status eingeben, kann über 256 hinausgehen, die in ein Byte passen. Wir müssen uns mit solchen Tricks auseinandersetzen.



Schauen wir uns nun den letzten Teil unseres Opcodes an, den Adressierungsmodus.







Wir haben 12 Adressierungsmodi. Wie wir bereits sagten, ermöglichen sie uns, die Anweisungen zu erhalten und anzugeben, woher die Daten stammen sollen.



Schauen wir uns drei Dinge an. Der letzte ist ABS, absoluter Adressierungsmodus. Beginnen wir damit. Ich entschuldige mich für eine kleine Verlegenheit. Er macht so etwas. Wir geben ihm die vollständige Adresse, 16 Bit, als Eingabe. Er holt uns den Wert aus dieser Speicherzelle. In Assembler können Sie in der zweiten Spalte sehen, wie es aussieht: LDA $ ccbb. ccbb ist eine Hexadezimalzahl, eine gewöhnliche Zahl, die einfach in einer anderen Notation geschrieben ist. Wenn Sie sich hier unwohl fühlen, denken Sie daran, dass dies nur eine Zahl ist.



In der dritten Spalte können Sie sehen, wie es im Maschinencode aussieht. Vor uns liegt der blau hervorgehobene Opcode - 173. Und 187 und 204 sind bereits Adressdaten. Da wir jedoch mit 8-Bit-Werten arbeiten, benötigen wir zwei Speicherplätze, um die Adresse zu schreiben.



Ich habe auch vergessen zu sagen, dass der Opcode für einige Zeit auf der CPU ausgeführt wird, es hat bestimmte Kosten. LDA mit absoluter Adressierung benötigt vier CPU-Zyklen.



Hier können Sie bereits verstehen, warum so viele Adressierungsmodi benötigt werden. Betrachten Sie den nächsten Adressierungsmodus, ZP0. Dies ist der Adressierungsmodus für Seite Null. Und Seite Null sind die ersten 256 Bytes, die im Speicher zugewiesen sind. Dies sind Adressen von Null bis 255.



Im Assembler wiederum LDA * 10. Was macht dieser Adressierungsmodus? Er sagt: gehe zu Seite Null, hier in diesen ersten 256 Bytes, mit so und so einem Offset. in diesem Fall 10, und nehmen Sie den Wert von dort. Hier stellen wir bereits einen signifikanten Unterschied zwischen den Adressierungsmodi fest.



Bei der absoluten Adressierung benötigten wir zunächst drei Bytes, um ein solches Programm zu schreiben. Zweitens brauchten wir vier CPU-Zyklen. Im ZP0-Adressierungsmodus wurden nur drei CPU-Zyklen und zwei Bytes benötigt. Aber ja, wir haben an Flexibilität verloren. Das heißt, wir können unsere Daten nur auf die erste Seite stellen, diese.



Der IMM für den endgültigen Adressierungsmodus lautet: Daten aus der Zelle neben dem Opcode entnehmen. Dieser LDA # 10 im Assembler macht das. Und es stellt sich heraus, dass das Programm wie [169, 10] aussieht. Es sind bereits zwei CPU-Zyklen erforderlich. Aber hier ist klar, dass wir auch an Flexibilität verlieren und den Opcode neben den Daten haben müssen.



Die Implementierung in JavaScript ist einfach. Hier ist ein Beispielcode. Es gibt eine Adresse. Dies ist die IMM-Adressierung, die Daten vom Programmzähler entnimmt. Wir sagen einfach, dass unsere Adresse ein Programmzähler ist, und erhöhen sie um eins, damit das Programm bei der nächsten Ausführung zum nächsten Befehl springt.



Hier ist eine lustige Sache. Wir können jetzt Maschinencode wie Frontend-Entwickler lesen. Und wir wissen sogar, wie man sieht, was dort im Assembler geschrieben steht.







Wir wissen bereits alles, was wir im Prinzip brauchen. Es gibt ein Programm, es besteht aus Bytes. Jedes Byte ist ein Opcode, jeder Opcode ist eine Anweisung und so weiter. Mal sehen, wie unser Programm ausgeführt wird. Und es wird nur in diesen CPU-Zyklen ausgeführt.



Wie kann ein solcher Code gemacht werden? Beispiel. Wir müssen den Opcode vom Programmzähler lesen und ihn dann einfach um eins erhöhen. Jetzt müssen wir diesen Opcode in einen Befehl und in den Adressierungsmodus dekodieren. Wenn Sie darüber nachdenken, ist der Opcode eine Primzahl, 169. Und in einem Byte haben wir nur 256 Zahlen. Wir können ein Array mit 256 Werten erstellen. Jedes Element dieses Arrays sendet uns einfach, welche Anweisung verwendet werden soll, welcher Adressierungsmodus benötigt wird und wie viele Zyklen es dauern wird. Das heißt, es ist super einfach. Und das Array, das ich habe, befindet sich gerade im Zustand des Prozessors.



Als nächstes führen wir einfach die Funktion des Adressierungsmodus in Zeile 36 aus, die uns die Adresse gibt, und geben Anweisungen.



Das Letzte, was wir tun müssen, ist, uns mit Schleifen zu befassen. opcodeResolver gibt die Anzahl der Zyklen zurück, wir schreiben sie in die verbleibende Variable Cycles. Wir betrachten jeden Prozessorzyklus: Wenn noch null Zyklen übrig sind, können wir den nächsten Befehl ausführen. Wenn er größer als null ist, verringern wir ihn einfach um eins. Und das ist alles super einfach. So wird das Programm auf 6502 ausgeführt. Wie







bereits erwähnt, kann sich das Programm in verschiedenen Teilen des Speichers, in verschiedenen Verhältnissen usw. befinden. Wie kann ein Prozessor verstehen, wo mit der Ausführung dieses Programms begonnen werden soll? Wir brauchen so ein int main aus der C-Welt.



In der Tat ist alles einfach. Der Prozessor hat eine Prozedur zum Zurücksetzen seines Status. In dieser Prozedur übernehmen wir die Adresse des Anfangsbefehls von der Adresse 0xfffxc. 0xfffxc ist wieder eine Hexadezimalzahl. Wenn Sie sich unwohl fühlen, punkten Sie, dies ist die übliche Zahl. So werden sie in JavaScript bis 0x geschrieben.



Wir müssen zwei Bytes der Adresse lesen, die Adresse ist 16 Bit. Wir lesen die niedrigen Bytes von dieser Adresse, die hohen Bytes von der nächsten Adresse. Und dann fügen wir diesen Fall mit einer solchen Magie von Bitoperationen hinzu. Durch Zurücksetzen des Prozessorstatus wird außerdem der Wert in den Registern zurückgesetzt - Register A, X, Y, Zeiger auf den Stapelstatus. Das Zurücksetzen dauert acht Zyklen. So ist das Ding.







Wir wissen jetzt schon alles. Um ehrlich zu sein, war es für mich schwierig, das alles zu schreiben, weil ich überhaupt nicht verstand, wie ich es testen sollte. Wir schreiben einen ganzen Computer, auf dem jedes Programm ausgeführt werden kann, das jemals dafür erstellt wurde. Wie kann man verstehen, dass wir uns richtig bewegen?



Es gibt einen großartigen und wunderbaren Weg! Wir nehmen zwei CPUs. Die erste ist die, die wir herstellen, die zweite ist die Referenz-CPU, wir wissen sicher, dass sie gut funktioniert. Zum Beispiel gibt es einen Emulator für das NES, den Nintendulator, der als eine solche Benchmark-CPU angesehen wird.



Wir nehmen ein bestimmtes Testprogramm, führen es auf der Referenz-CPU aus und schreiben den Prozessorstatus für jeden Befehl in das Statusprotokoll. Dann nehmen wir dieses Programm und führen es auf unserer CPU aus. Und jeder Zustand nach jedem Befehl wird mit diesem Protokoll verglichen. Super Idee!



Natürlich brauchen wir keine CPU-Referenz. Wir brauchen nur ein Programmausführungsprotokoll. Dieses Protokoll finden Sie auf Nesdev. Ich weiß nicht, dass ein Prozessoremulator in ein paar Tagen am Wochenende geschrieben werden kann - es ist einfach großartig!



Und alle. Wir nehmen das Protokoll, vergleichen den Status und haben einen interaktiven Test. Wir führen den ersten Befehl aus, er ist nicht in dem Prozessor implementiert, den wir entwickeln. Wir implementieren es, gehen zur nächsten Zeile des Protokolls und implementieren es erneut. Super schnell! Ermöglicht es Ihnen, sich schnell zu bewegen.



NES-Architektur



Wir haben jetzt eine CPU, die im Wesentlichen das Herz unseres Computers ist. Und wir können sehen, woraus die Architektur des NES selbst besteht und wie solche komplexen zusammengesetzten Computersysteme hergestellt werden. Denn wenn Sie darüber nachdenken, gibt es eine CPU, es gibt Speicher. Wir können Werte empfangen, aufnehmen usw.







Aber im NES gibt es in jeder Set-Top-Box auch einen Bildschirm, Audiogeräte usw. Wir müssen lernen, mit Peripheriegeräten zu arbeiten. Sie müssen dafür nicht einmal etwas Neues lernen, das Konzept unseres Busses reicht aus. Dies ist wahrscheinlich die zweite so brillante Idee, eine brillante Entdeckung, die ich mir beim Schreiben eines Emulators gemacht habe.



Stellen wir uns vor, wir nehmen unseren Speicher, der 64 Kilobyte betrug, und teilen ihn in zwei 32-Kilobyte-Bereiche auf. Im unteren Bereich befindet sich ein bestimmtes Gerät, bei dem es sich um eine Reihe von Glühbirnen handelt, wie auf dem Bild mit dieser Platine dargestellt.



Nehmen wir an, beim Schreiben in diesen 32-Kilobyte-Junior-Bereich geht das Licht an oder aus. Wenn wir dort den Wert 1 schreiben, geht das Licht an, wenn 0 - erlischt. Gleichzeitig können wir den Wert lesen und den Status des Systems verstehen, verstehen, welches Bild auf diesem Bildschirm angezeigt wird.



Wiederum legen wir im oberen Adressbereich einen normalen Speicher ab, in dem sich das Programm befindet, da wir die Adresse während des Rücksetzvorgangs im oberen Bereich benötigen.



Das ist eigentlich eine super geniale Idee. Für die Interaktion mit Peripheriegeräten sind keine zusätzlichen Befehle usw. erforderlich. Wir schreiben wie bisher nur in einen guten alten Speicher. Gleichzeitig kann der Speicher aber bereits zusätzliche Geräte sein.







Wir sind jetzt voll und ganz bereit, einen Blick auf die NES-Architektur zu werfen. Wir haben wie immer eine CPU und ihren Bus. Es gibt zwei zusätzliche Kilobyte Speicher. Es gibt eine APU - ein Tonausgabegerät. Leider werden wir es jetzt nicht berücksichtigen, aber auch dort ist alles super cool. Und da ist eine Patrone. Es befindet sich im hohen Bereich und liefert Programmdaten. Er liefert auch diese Grafiken, jetzt werden wir betrachten. Das Letzte auf dem CPU-Bus ist die PPU, die Bildverarbeitungseinheit, eine solche Proto-Grafikkarte. Wenn Sie lernen möchten, wie man mit Grafikkarten arbeitet, lernen wir jetzt sogar, wie man eine implementiert.



Die PPU hat auch einen eigenen Bus, auf dem die Namenstabellen, Paletten und Grafikdaten verschoben werden. Die Grafikdaten stammen jedoch aus der Kassette. Und dann ist da noch das Gedächtnis des Objekts. Das ist die Architektur.







Mal sehen, was eine Patrone ist. Dies ist eine viel coolere Idee als die CD, wenn man bedenkt, dass sie aus der Vergangenheit stammt.



Warum ist sie cool? Links sehen wir die Patrone der amerikanischen Region, das berühmte Spiel Zelda, wenn jemand nicht gespielt hat - spielen, super. Und wenn wir diese Patrone zerlegen, finden wir darin Mikroschaltungen. Es gibt keine Laserscheibe usw. Normalerweise enthalten diese Chips nur einige Daten. Außerdem schneidet die Kassette direkt in unser Computersystem, in den CPU- und PPU-Bus. Es ermöglicht Ihnen, erstaunliche Dinge zu tun und die Benutzererfahrung zu verbessern.



An Bord der Kassette befindet sich ein Mapper, der mit der Übersetzung von Adressen gefüllt ist. Nehmen wir an, wir haben ein großes Spiel. Das NES verfügt jedoch nur über 32 Kilobyte Speicher, den es für das Programm adressieren kann. Ein Spiel hat zum Beispiel 128 Kilobyte. Mapper kann während der Programmausführung im laufenden Betrieb einen bestimmten Speicherbereich durch völlig neue Daten ersetzen. Wir können im Programm sagen: Laden Sie uns Level 2, und der Speicher wird fast sofort direkt ersetzt.



Außerdem gibt es lustige Dinge. Der Mapper kann beispielsweise Chips liefern, die Soundtracks erweitern, neue hinzufügen usw. Wenn Sie Castlevania gespielt haben, hören Sie, wie sich Castlevania in der japanischen Region anhört. Es gibt einen zusätzlichen Sound, der sich ganz anders anhört. In diesem Fall wird alles auf derselben Hardware ausgeführt. Das heißt, diese Idee ähnelt eher der, als Sie eine Grafikkarte gekauft, an einen Computer angeschlossen und über zusätzliche Funktionen verfügen. Hier ist es genauso. Das ist super. Aber wir stecken mit CDs fest.







Fahren wir mit dem letzten Teil fort - schauen wir uns an, wie dieses Bildausgabegerät funktioniert. Denn wenn Sie einen Emulator erstellen möchten, besteht das Mindestprogramm darin, einen Prozessor zu erstellen und zu beobachten, wie Bilder und Videospiele aussehen.



Beginnen wir mit der Entität der obersten Ebene - dem Bild selbst. Es hat zwei Pläne. Es gibt einen Vordergrund, in dem dynamischere Objekte platziert werden, und einen Hintergrund, in dem statischere Objekte wie eine Szene platziert werden.



Sie können die Aufteilung hier sehen. Auf der linken Seite befindet sich das gleiche berühmte Castlevania-Spiel, daher wird unsere gesamte Reise zur PPU mit Simon Belmont stattfinden. Gemeinsam mit ihm werden wir überlegen, wie alles funktioniert.



Es gibt einen Hintergrund, Spalten usw. Wir sehen, dass sie im Hintergrund gezeichnet sind, aber gleichzeitig sind alle Zeichen - Simon selbst (links, braun) und Geister - bereits im Vordergrund gezeichnet. Das heißt, der Vordergrund ist für dynamischere Entitäten vorhanden, und der Hintergrund ist für statischere Entitäten vorhanden.







Ein Bild auf einer Bitmap-Anzeige besteht aus Pixeln. Pixel sind nur farbige Punkte. Zumindest brauchen wir Farben. Das NES verfügt über eine Systempalette. Es besteht aus 64 Farben, was leider alle Farben sind, die das NES reproduzieren kann. Wir können jedoch keine Farbe aus der Palette entnehmen. Für benutzerdefinierte Paletten gibt es einen bestimmten Speicherbereich, der wiederum in zwei solche Unterbereiche unterteilt ist.



Es gibt eine Reihe von Hintergrund und Vordergrund. Jeder Bereich ist in vier Paletten mit vier Farben unterteilt. Die Palette "Hintergrund, Null" besteht beispielsweise aus Weiß, Blau und Rot. Und die vierte Farbe in jeder Palette bezieht sich immer auf eine transparente Farbe, mit der wir ein transparentes Pixel erstellen können.







Dieser Bereich mit Paletten befindet sich nicht mehr auf dem CPU-Bus, sondern auf dem PPU-Bus. Mal sehen, wie wir dort Daten schreiben können, weil wir über den CPU-Bus keinen Zugriff auf den PPU-Bus haben.



Hier kommen wir noch einmal auf die Idee der Speicherzuordnung I / O zurück. Es gibt Adressen 0x2006 und 0x2007, dies sind hexadezimale Adressen, aber es sind nur Zahlen. Und wir schreiben so. Da unsere Adresse 16-Bit ist, schreiben wir die Adresse in zwei Ansätzen von acht Bit in das Adressregister ox2006 und können dann unsere Daten über die Adresse 0x2007 schreiben. So eine lustige Sache. Das heißt, wir müssen drei Operationen ausführen, um zumindest etwas in die Palette zu schreiben.







Ausgezeichnet. Wir haben eine Palette, aber wir brauchen Strukturen. Farben sind immer gut, aber Bitmaps haben eine bestimmte Struktur.



Für Grafiken gibt es zwei Tabellen mit jeweils vier Kilobyte, die Kacheln enthalten. Und all diese Erinnerungen sind eine Art Atlas. Wenn zuvor alle ein Rasterbild verwendeten, erstellten sie einen großen Atlas, aus dem sie dann die erforderlichen Bilder durch das Hintergrundbild nach Koordinaten auswählten. Hier ist die gleiche Idee.



Jede Tabelle hat 256 Kacheln. Wieder eine lustige Numerologie: Mit genau 256 können Sie ein Byte und 256 verschiedene Werte angeben. Das heißt, in einem Byte können wir jede Kachel angeben, die wir benötigen. Es stellt sich heraus, zwei Tabellen. Eine Tabelle für Hintergründe, eine andere für Vordergrund.







Mal sehen, wie diese Kacheln gespeichert werden. Auch hier ist es eine lustige Sache. Denken wir daran, dass unsere Palette vier Farben enthält. Nochmals Numerologie: Ein Byte hat acht Bits und eine Kachel ist acht mal acht. Es stellt sich heraus, dass wir mit einem Byte einen Streifen einer Kachel darstellen können, wobei jedes Bit für eine bestimmte Farbe verantwortlich ist. Und mit acht Bytes können wir eine vollwertige Kachel von acht mal acht darstellen.



Aber hier gibt es ein Problem. Wie gesagt, ein Bit ist für die Farbe verantwortlich, kann aber nur zwei Werte darstellen. Fliesen werden in zwei Ebenen gespeichert. Es gibt eine Ebene mit dem höchstwertigen und dem niedrigstwertigen Bit. Um die endgültige Farbe zu erhalten, kombinieren wir Daten aus beiden Ebenen.



Sie können betrachten - hier zum Beispiel der Buchstabe "I", der untere Teil, da ist die Zahl "3", die sich so herausstellt: Wir nehmen die Ebene der niedrigstwertigen und höchstwertigen Bits und erhalten die Binärzahl 11, die gleich der Dezimalzahl 3 ist. Eine so lustige Datenstruktur.



Hintergrund



Jetzt können wir endlich den Hintergrund rendern!







Es gibt eine Tabelle mit Namen dafür. Wir haben zwei davon, jedes mit 960 Bytes, jedes Byte verweist auf eine bestimmte Kachel. Das heißt, die Kachelkennung ist in der vorherigen Tabelle angegeben. Wenn wir diese 960 Bytes als Matrix darstellen, erhalten wir einen 32 x 30-Kachelbildschirm. Die NES-Auflösung beträgt 256 x 240 Pixel.



Ausgezeichnet. Wir können dort Kacheln schreiben. Wie Sie vielleicht bemerkt haben, geben die Kacheln nicht die Palette an, mit der sie angezeigt werden sollen. Wir können verschiedene Kacheln mit verschiedenen Paletten anzeigen und müssen diese Informationen auch irgendwo speichern. Leider haben wir nur 64 Bytes pro Namenstabelle zum Speichern von Paletteninformationen.



Und hier tritt das Problem auf. Wenn wir die Tabelle weiter aufteilen, sodass nur 64 Werte vorhanden sind, erhalten wir Quadrate mit vier mal vier Kacheln, die wie ein rotes Quadrat aussehen. Dies ist nur ein großer Teil des Bildschirms. Sie würde einer Palette untergeordnet sein, wenn nicht einer.



Wie wir uns erinnern, gibt es vier Paletten in der Unterpalette, und wir brauchen nur zwei Bits, um diejenige anzuzeigen, die wir brauchen. Jedes dieser 64 Bytes kopiert die Paletteninformationen für ein Vier-mal-Vier-Raster. Dieses Gitter ist jedoch immer noch zu zweit in solche Teilgitter unterteilt. Natürlich gibt es hier eine Einschränkung: Ein Zwei-mal-Zwei-Raster ist an eine Palette gebunden. Dies sind die Einschränkungen in der Welt der Anzeige von Hintergründen auf Nintendo. Eine lustige Tatsache, aber im Allgemeinen stört es die Spiele nicht wirklich.







Es wird auch gescrollt. Wenn wir uns zum Beispiel an "Mario" oder Castlevania erinnern, dann wissen wir: Wenn sich der Held in diesen Spielen nach rechts bewegt, scheint sich die Welt entlang des Bildschirms zu entfalten. Dies erfolgt durch Scrollen.



Denken Sie daran, dass wir zwei Namenstabellen haben, die bereits zwei Bildschirme codieren. Und wenn sich unser Held bewegt, fügen wir der folgenden Namenstabelle Daten hinzu. Wenn sich unser Held sofort bewegt, füllen wir die Namenstabelle aus. Es stellt sich heraus, dass wir angeben können, von welcher Kachel in der Namenstabelle wir Daten anzeigen möchten, und wir werden sie in Streifen erweitern. Der ganze Trick beim Scrollen besteht darin, aus den beiden Namenstabellen zu lesen.



Das heißt, wenn wir horizontal über eine Tabelle mit Namen hinausgehen, beginnen wir automatisch aus einer anderen zu lesen usw. Und vergessen Sie nicht, die Daten erneut auszufüllen.



Das Scrollen war damals übrigens eine ziemlich große Sache. John Carmacks erste Erfolge betrafen das Scrollen. Schauen Sie sich diese Geschichte an, sie ist ziemlich lustig.



Vordergrund



Und der Vordergrund. Im Vordergrund stehen, wie gesagt, dynamische Entitäten, die im Speicher von Objekten und Attributen gespeichert sind.







Es gibt 256 Bytes, in die wir 64 Objekte schreiben können, vier Bytes pro Objekt. Jedes Objekt codiert X und Y, dh den Pixelversatz auf dem Bildschirm. Plus Kacheladresse und Attribute. Wir können den Hintergrund priorisieren, siehe Bild unten? Wir können die Palette angeben. Die Priorität vor dem Hintergrund teilt der PPU mit, dass der Hintergrund über dem Sprite gezeichnet werden soll. Dadurch können wir Simon hinter die Statue stellen.



Wir können auch eine Ausrichtung vornehmen und sie über eine beliebige Achse drehen, z. B. horizontal oder vertikal, wie der Buchstabe "I" im Bild. Wir schreiben ungefähr so ​​wie die Palette: über die Adresse 0x2003, 0x2004.



Endlich das Finale. Wie rendern wir Vordergrundobjekte?







Das Bild entfaltet sich entlang von Linien, die als Scanlinien bezeichnet werden. Dies ist ein Fernsehbegriff. Vor jeder Scanlinie greifen wir einfach acht Sprites aus dem Objekt- und Attributspeicher. Nicht mehr als acht, nur acht werden unterstützt. Es gibt auch eine solche Einschränkung. Wir zeigen sie nur zeilenweise an, wie hier zum Beispiel. Auf der aktuellen Scanlinie zeigen wir in Gelb eine Wolke, eine Sonne und ein Herz in einem Streifen. Und der Smiley wird nicht angezeigt. Aber er ist immer noch glücklich.



Schauen Sie sich den One Lone Coder- Superkanal an . Es gibt insbesondere den Programmierprozess selbst - das Programmieren des NES-Emulators. Und Nesdev enthält alle Informationen zur Emulation - woraus sie besteht usw. Der letzte Link ist der Code meines Emulators . Schauen Sie bei Interesse vorbei. Geschrieben in TypeScript.



Danke. Hoffe es hat euch gefallen.



All Articles