Le rapport peut être divisé en trois parties:
- comment fonctionne le processeur 6502 et comment l'émuler à l'aide de JavaScript,
- comment fonctionne le périphérique de sortie graphique et comment les jeux stockent leurs ressources,
- comment le son est synthétisé à l'aide de l'audio Web et comment il est parallélisé en deux flux à l'aide d'un orlet audio.
J'ai essayé de donner des conseils sur l'optimisation. Pourtant, l'émulation est la chose, à 60 FPS, il reste peu de temps pour l'exécution du code.
- Bonjour à tous, je m'appelle Zhenya. Maintenant, il y aura une petite discussion inhabituelle, samedi, sur le projet pendant de nombreux samedis. Parlons de l'émulation des systèmes informatiques, qui peut être implémentée en plus des technologies Web existantes. En fait, le Web est déjà assez riche en outils et vous pouvez faire des choses absolument incroyables. Plus précisément, nous parlerons de l'émulateur à tous, probablement, la célèbre console Dandy des années 90, qui s'appelle en fait la Nintendo Entertainment System.
Souvenons-nous d'un peu d'histoire. Cela a commencé en 1983 lorsque la Famicom est sortie au Japon. Il a été publié par Nintendo. En 1985, la version américaine est sortie, appelée Nintendo Entertainment System. Dans les années 90, nous avions la même région taïwanaise appelée Dandy, mais secrètement, il s'agit d'un préfixe non officiel. Et le dernier cadeau en fer de Nintendo de la part de Nintendo remonte à 2016, lorsque la NES mini est sortie. Malheureusement, je n'ai pas de mini NES. Il y a SNES mini, Super Nintendo. Regardez quelle petite chose, et sur cette diapositive, vous pouvez voir la loi de Moore dans toute sa splendeur.
Si nous regardons 1985 et le rapport de la console au joystick, et en 2016, nous pouvons voir à quel point tout est devenu plus petit, car les mains des gens ne changent pas, le joystick ne peut pas être réduit, mais la console elle-même est devenue minuscule.
Comme nous l'avons déjà remarqué, il existe de nombreux émulateurs. Nous ne l'avons pas dit, mais au moins un fonctionnaire l'a remarqué. Ce truc - SNES mini ou NES mini - n'est en fait pas un vrai décodeur. Il s'agit d'un élément matériel qui émule la console. En fait, il s'agit d'un émulateur officiel, mais qui se présente sous une forme de fer si drôle.
Mais comme nous le savons, depuis les années 2000, il existe des programmes qui émulent la NES, grâce auxquels nous pouvons encore profiter des jeux de cette époque. Et il existe de nombreux émulateurs. Pourquoi un autre, en particulier en JavaScript, me demandez-vous? Quand j'ai fait cette chose, j'ai trouvé trois réponses à cette question pour moi-même.
- , . - , . . , - , - . . . , , . , -.
- , , . , , , , NES — , , NTSC, 60 . 16 , . .
- . , . , , . , , — , . . , , .
J'ai également regardé une présentation de Matt Godbold, qui a également parlé d'émuler le processeur qui exécute la NES. Il a dit que c'était drôle que nous imitions une chose de si bas niveau dans un langage de si haut niveau. Nous n'avons pas accès au matériel, nous travaillons indirectement.
Passons à l'examen de ce que nous allons émuler, comment nous allons émuler, etc. Nous allons commencer par le processeur. La NES elle-même est emblématique. Pour la Russie, c'est clair, c'est un phénomène culturel. Mais en Occident, et en Orient, au Japon, c'était aussi un phénomène culturel, car la console, en fait, a sauvé toute l'industrie du jeu vidéo domestique.
Le processeur est également installé dans l'emblématique MOS6502. Quelle est sa signification? Au moment où il est apparu, ses concurrents étaient au prix de 180 $ et le MOS6502 au prix de 25 $. Autrement dit, ce processeur a lancé la révolution de l'ordinateur personnel. Et ici, j'ai deux ordinateurs. Le premier est Apple II, nous savons tous et imaginons à quel point cet événement a été important pour le monde des ordinateurs personnels.
Il y a aussi un ordinateur BBC Micro. Il était plus populaire en Grande-Bretagne, la BBC est une société de télévision britannique. Autrement dit, ce processeur a amené les ordinateurs aux masses, grâce à lui, nous sommes maintenant des programmeurs, des développeurs frontaux.
Jetons un coup d'œil au programme minimum. De quoi avons-nous besoin pour créer un système informatique?
Le processeur lui-même est un appareil assez inutile. Comme nous le savons, la CPU exécute le programme. Mais au moins pour que ce programme soit stocké quelque part, de la mémoire est nécessaire. Et, bien sûr, il est inclus dans le programme minimum. Et notre mémoire est constituée de cellules de huit bits, appelées octets.
En JavaScript, nous pouvons utiliser des tableaux Uint8Array typés pour émuler cette mémoire, c'est-à-dire que nous pouvons allouer un tableau.
Pour que la mémoire s'interface avec le processeur, il existe un bus. Le bus permet au processeur d'adresser la mémoire via des adresses. Les adresses ne sont plus constituées de huit bits, comme des données, mais de 16, ce qui nous permet d'adresser 64 kilo-octets de mémoire.
Il y a un certain état dans le processeur, il y a trois registres - A, X, Y. Un registre est comme un stockage de valeurs intermédiaires. La taille du registre est d'un octet ou de huit bits. Cela nous indique que le processeur est de huit bits, il fonctionne sur des données de huit bits.
Un exemple d'utilisation du registre. Nous voulons ajouter deux nombres, mais il n'y a qu'un seul bus en mémoire. Il s'avère que vous devez stocker le premier numéro quelque part entre les deux. Nous la sauvegardons dans le registre A, nous pouvons prendre la deuxième valeur de la mémoire, les ajouter, et le résultat est à nouveau placé dans le registre A.
Fonctionnellement, ces registres sont assez indépendants - ils peuvent être utilisés comme des registres généraux. Mais ils ont une signification, telle que l'addition, le résultat est obtenu dans le registre A et la valeur du premier opérande est prise.
Ou, par exemple, nous adressons des données. Nous en reparlerons un peu plus tard. Nous pouvons spécifier le mode d'adressage offset et utiliser le registre X pour obtenir la valeur finale.
Qu'est-ce qui est inclus dans l'état du processeur? Il existe un registre PC qui pointe vers l'adresse de la commande en cours, car l'adresse est de deux octets.
Nous avons également le registre d'état, qui indique les indicateurs d'état. Par exemple, si nous soustrayons deux valeurs et obtenons une valeur négative, alors un certain bit dans le registre d'indicateur est allumé.
Enfin, il y a SP, un pointeur vers la pile. La pile n'est que de la mémoire ordinaire, elle n'est pas séparée de tout le reste, de tous les autres programmes. Il y a simplement une instruction du processeur qui contrôle ce pointeur SP. C'est ainsi que la pile est implémentée. Ensuite, nous examinerons une excellente idée informatique qui mène à des solutions aussi intéressantes.
Maintenant, nous savons qu'il y a un processeur, une mémoire, un état dans le processeur. Voyons quel est notre programme. Il s'agit d'une séquence d'octets. Cela n'a même pas besoin d'être cohérent. Le programme lui-même peut être situé dans différentes parties de la mémoire.
Nous pouvons imaginer un programme, j'ai ici un morceau de code - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. C'est un vrai programme pour 6502. Chaque octet de ce programme, chaque chiffre de ce tableau est entité comme opcode. Opcode - code d'opération. «Puis, encore une fois, un nombre ordinaire.
Par exemple, il existe un opcode 169. Il code deux choses en lui-même - d'abord, une instruction. Lorsqu'elle est exécutée, l'instruction change l'état du processeur, de la mémoire, etc., c'est-à-dire l'état du système. Par exemple, nous ajoutons deux nombres, le résultat apparaît dans le registre A. Ceci est un exemple d'instruction. Nous avons également une instruction LDA, que nous examinerons plus en détail. Il charge une valeur de la mémoire dans le registre A.
La deuxième chose que l'opcode encode est le mode d'adressage. Il donne des instructions pour savoir où obtenir ses données. Par exemple, s'il s'agit du mode d'adressage IMM, alors il dit: prenez les données qui se trouvent dans la cellule à côté du compteur de programme actuel. Nous verrons également comment fonctionne ce mode et comment il peut être implémenté en JavaScript.
Tel est le programme. En dehors de ces octets, tout est très similaire à JavaScript, mais à un niveau inférieur.
Si vous vous souvenez de ce dont je parlais, il peut y avoir un drôle de paradoxe. Il s'avère que nous stockons le programme en mémoire, ainsi que les données. On pourrait se poser cette question: un programme peut-il servir de données? La réponse est oui. Nous pouvons changer du programme lui-même au moment de l'exécution de ce programme.
Ou une autre question: les données peuvent-elles être un programme? Oui aussi. Le processeur n'a pas d'importance. Il broie simplement, comme un moulin, les octets qui lui sont fournis et suit les instructions. Une chose paradoxale. Si vous y réfléchissez, c'est super peu sûr. Vous pouvez commencer à exécuter un programme qui ne contient que des données sur une pile, etc. Mais l'avantage est que c'est super facile. Pas besoin de faire des circuits compliqués.
C'est la première grande idée que nous rencontrons aujourd'hui. C'est ce qu'on appelle l'architecture von Neumann. Mais il y avait en fait de nombreux coauteurs là-bas.
Voici illustré. Il y a le programme 1, opcode 169, suivi de 10, quelques données. D'accord. Ce programme peut également être visualisé comme ceci: 169 sont des données et 10 est un opcode. Ce sera un programme légal pour 6502. Ce programme entier, encore une fois, peut être considéré comme des données.
Si nous avons un compilateur, nous pouvons construire quelque chose, le mettre dans ce morceau de mémoire, et ce sera une chose tellement drôle.
Jetons un coup d'œil à la première partie de notre programme - les instructions.
6502 donne accès à 73 instructions, y compris l'arithmétique: addition, soustraction. Pas de multiplication et de division, désolé. Il y a des opérations sur les bits, il s'agit de manipuler des bits dans des mots de huit bits.
Il y a des sauts qui sont interdits dans notre frontend: l'instruction jump, qui transfère simplement le compteur de programme vers une partie du code. Ceci est interdit en programmation, mais si vous avez affaire à un niveau bas, c'est la seule façon de faire du branchement. Il y a des opérations pour la pile, etc. Elles sont regroupées. Oui, nous avons 73 instructions, mais si vous regardez les groupes et ce qu'ils font, il n'y en a vraiment pas beaucoup et ils sont tous assez similaires.
Revenons à l'instruction LDA. Comme nous l'avons dit, il s'agit de "charger la valeur de la mémoire dans le registre A". C'est à quel point cela peut être super simple en JavaScript. A l'entrée se trouve l'adresse que nous fournit le mode d'adressage. Nous changeons l'état à l'intérieur, nous disons que this._a est égal à la valeur lue depuis la mémoire.
Nous devons encore définir ces deux champs de bits dans le registre d'état - un indicateur zéro et un indicateur négatif. Il y a beaucoup de choses au niveau du bit ici. Mais si vous créez un émulateur, cela devient une seconde nature de faire ces OR, négatifs, etc. La seule chose amusante ici est qu'il y a un tel% 256 dans la deuxième branche. Cela nous renvoie, encore une fois, à la nature de notre langage JavaScript bien-aimé, au fait qu'il n'a pas de valeurs typées. La valeur que nous mettons dans Status peut aller au-delà de 256, qui tiennent dans un octet. Nous devons faire face à de telles astuces.
Regardons maintenant la dernière partie de notre opcode, le mode d'adressage.
Nous avons 12 modes d'adressage. Comme nous l'avons déjà dit, ils nous permettent d'obtenir et d'indiquer pour l'instruction où obtenir les données.
Jetons un coup d'œil à trois choses. Le dernier est ABS, mode d'adressage absolu, commençons par lui, je m'excuse pour un petit embarras. Il fait quelque chose comme ça. Nous lui donnons l'adresse complète, 16 bits, en entrée. Il nous tire la valeur de cette cellule mémoire. En assembleur, dans la deuxième colonne, vous pouvez voir à quoi il ressemble: LDA $ ccbb. ccbb est un nombre hexadécimal, un nombre ordinaire, simplement écrit dans une notation différente. Si vous vous sentez mal à l'aise ici, rappelez-vous que ce n'est qu'un chiffre.
Dans la troisième colonne, vous pouvez voir à quoi il ressemble dans le code machine. Devant se trouve l'opcode - 173, surligné en bleu. Et 187 et 204 sont déjà des données d'adresse. Mais comme nous fonctionnons avec des valeurs de huit bits, nous avons besoin de deux emplacements de mémoire pour écrire l'adresse.
J'ai aussi oublié de dire que l'opcode est exécuté pendant un certain temps sur le CPU, il a un certain coût. LDA avec adressage absolu prend quatre cycles CPU.
Ici, vous pouvez déjà comprendre pourquoi tant de modes d'adressage sont nécessaires. Considérez le mode d'adressage suivant, ZP0. C'est le mode d'adressage de la page zéro. Et la page zéro correspond aux 256 premiers octets alloués en mémoire. Ce sont des adresses de zéro à 255.
En assembleur, encore une fois, LDA * 10. Que fait ce mode d'adressage? Il dit: allez à la page zéro, ici dans ces 256 premiers octets, avec tel ou tel décalage. dans ce cas 10, et prenez la valeur à partir de là. On remarque déjà ici une différence significative entre les modes d'adressage.
Dans le cas de l'adressage absolu, il nous fallait d'abord trois octets pour écrire un tel programme. Deuxièmement, nous avions besoin de quatre cycles CPU. Et en mode d'adressage ZP0, il n'a fallu que trois cycles CPU et deux octets. Mais oui, nous avons perdu de la flexibilité. Autrement dit, nous ne pouvons mettre nos données que sur la première page, celle-ci.
Le mode d'adressage final IMM dit: prenez les données de la cellule à côté de l'opcode. Ce LDA # 10 en assembleur fait cela. Et il s'avère que le programme ressemble à [169, 10]. Il nécessite déjà deux cycles CPU. Mais ici, il est clair que nous perdons également de la flexibilité, et nous avons besoin de l'opcode pour être à côté des données.
L'implémentation de cela en JavaScript est facile. Voici un exemple de code. Il y a une adresse. Il s'agit de l'adressage IMM, qui prend les données du compteur de programme. Nous disons simplement que notre adresse est un compteur de programme et l'incrémentons de un pour que la prochaine fois que le programme est exécuté, il passe à l'instruction suivante.
Voici une chose amusante. Nous pouvons maintenant lire le code machine comme les développeurs frontend. Et on sait même voir ce qui y est écrit dans l'assembleur.
Nous savons déjà tout ce dont nous avons besoin en principe. Il y a un programme, il se compose d'octets. Chaque octet est un opcode, chaque opcode est une instruction, etc. Voyons comment notre programme est exécuté. Et il est exécuté uniquement dans ces cycles CPU.
Comment faire un tel code? Exemple. Nous devons lire l'opcode à partir du compteur de programme, puis l'augmenter simplement d'un. Nous devons maintenant décoder cet opcode en une instruction et en mode d'adressage. Si vous y réfléchissez bien, l'opcode est un nombre premier, 169. Et dans un octet, nous n'avons que 256 nombres. Nous pouvons créer un tableau avec 256 valeurs. Chaque élément de ce tableau nous référera simplement à quelle instruction utiliser, quel mode d'adressage est nécessaire et combien de cycles cela prendra. Autrement dit, c'est super simple. Et le tableau que j'ai est juste dans l'état du processeur.
Ensuite, nous effectuons simplement la fonction du mode d'adressage sur la ligne 36, qui nous donne l'adresse, et lui fournissons des instructions.
La dernière chose à faire est de gérer les boucles. opcodeResolver renvoie le nombre de cycles, nous les écrivons dans la variable restanteCycles. Nous regardons chaque cycle du processeur: s'il ne reste aucun cycle, alors nous pouvons exécuter la commande suivante, si elle est supérieure à zéro, nous la diminuons simplement de un. Et c'est tout, super simple. C'est ainsi que le programme est exécuté sur 6502.
Mais comme nous l'avons déjà dit, le programme peut être dans différentes parties de la mémoire, dans des rapports différents, etc. Comment un processeur peut-il comprendre par où commencer l'exécution de ce programme? Nous avons besoin d'un int main comme celui-ci du monde C.
En fait, tout est simple. Le processeur dispose d'une procédure pour réinitialiser son état. Dans cette procédure, nous prenons l'adresse de la commande initiale de l'adresse 0xfffxc. 0xfffxc est à nouveau un nombre hexadécimal. Si vous vous sentez mal à l'aise, notez, c'est le nombre habituel. C'est ainsi qu'ils sont écrits en JavaScript, via 0x.
Nous devons lire deux octets de l'adresse, l'adresse est de 16 bits. Nous lisons les octets bas de cette adresse, les octets hauts de l'adresse suivante. Et puis nous ajoutons ce cas avec une telle magie d'opérations sur les bits. De plus, la réinitialisation de l'état du processeur réinitialise également la valeur dans les registres - registre A, X, Y, pointeur vers la pile, état. La réinitialisation prend huit cycles. Telle est la chose.
Nous savons déjà tout maintenant. Pour être honnête, c'était un peu difficile pour moi d'écrire tout cela, car je ne comprenais pas du tout comment le tester. Nous écrivons un ordinateur entier qui peut exécuter n'importe quel programme jamais créé pour lui. Comment comprendre qu'on avance correctement?
Il existe un moyen superbe et merveilleux! Nous prenons deux processeurs. Le premier est celui que nous fabriquons, le second est le CPU de référence, nous savons avec certitude qu'il fonctionne bien. Par exemple, il existe un émulateur pour le NES, le nintendulator, qui est considéré comme une référence pour les processeurs.
Nous prenons un certain programme de test, l'exécutons sur la CPU de référence et écrivons l'état du processeur dans le journal d'état pour chaque commande. Ensuite, nous prenons ce programme et l'exécutons sur notre CPU. Et chaque état après chaque commande est comparé à ce journal. Super idée!
Bien sûr, nous n'avons pas besoin d'une référence CPU. Nous avons juste besoin d'un journal d'exécution du programme. Ce journal peut être trouvé sur Nesdev. En fait, un émulateur de processeur peut être écrit, je ne sais pas, en quelques jours le week-end - c’est tout simplement superbe!
Et c'est tout. Nous prenons le journal, comparons l'état et nous avons un test interactif. Nous exécutons la première commande, elle n'est pas implémentée dans le processeur que nous développons. Nous l'implémentons, passons à la ligne suivante du journal et l'implémentons à nouveau. Super rapide! Vous permet de vous déplacer rapidement.
Architecture NES
Nous avons maintenant un processeur, qui est essentiellement le cœur de notre ordinateur. Et nous pouvons voir de quoi est faite l'architecture de la NES elle-même et comment ces systèmes informatiques composites complexes sont fabriqués. Parce que si vous y réfléchissez, eh bien, il y a un processeur, il y a de la mémoire. Nous pouvons recevoir des valeurs, enregistrer, etc.
Mais dans la NES, dans n'importe quel décodeur, il y a aussi un écran, des appareils audio, etc. Nous devons apprendre à travailler avec des périphériques. Vous n'avez même pas besoin d'apprendre quoi que ce soit de nouveau pour cela, le concept de notre bus suffit. C'est probablement la deuxième idée aussi brillante, une brillante découverte que j'ai faite pour moi-même en écrivant un émulateur.
Imaginons que nous prenions notre mémoire, qui était de 64 kilo-octets, et la divisions en deux plages de 32 kilo-octets. Dans la plage inférieure, il y aura un certain appareil, qui est un tableau d'ampoules, comme sur l'image avec cette carte.
Disons que lorsque vous écrivez sur cette plage junior de 32 kilo-octets, la lumière s'allume ou s'éteint. Si nous y écrivons la valeur 1, la lumière s'allume, si 0 - s'éteint. En même temps, nous pouvons lire la valeur et comprendre l'état du système, comprendre quelle image est affichée sur cet écran.
Encore une fois, dans la plage d'adresses supérieure, nous mettons la mémoire ordinaire dans laquelle se trouve le programme, car nous avons besoin d'une adresse dans la plage supérieure pendant la procédure de réinitialisation.
C'est en fait une idée super géniale. Pour interagir avec les périphériques, aucune commande supplémentaire n'est nécessaire, etc. Nous écrivons simplement dans une bonne vieille mémoire, comme auparavant. Mais en même temps, la mémoire peut déjà être des périphériques supplémentaires.
Nous sommes maintenant tout à fait prêts à jeter un coup d'œil à l'architecture NES. Nous avons un CPU et son bus, comme d'habitude. Il y a deux kilo-octets supplémentaires de mémoire. Il y a un APU - un périphérique de sortie audio. Malheureusement, maintenant nous ne l'envisagerons pas, mais tout y est super cool aussi. Et il y a une cartouche. Il est placé dans la gamme haute et fournit des données de programme. Il fournit également ces graphiques, nous allons maintenant les considérer. La dernière chose sur le bus CPU est le PPU, l'unité de traitement d'image, telle une carte proto-vidéo. Si vous souhaitez apprendre à travailler avec des cartes vidéo, nous allons même maintenant apprendre à en implémenter une.
Le PPU dispose également de son propre bus, sur lequel les tables de noms, les palettes et les données graphiques sont décalées. Mais les données graphiques proviennent de la cartouche. Et puis il y a la mémoire de l'objet. Telle est l'architecture.
Voyons ce qu'est une cartouche. C'est une idée beaucoup plus cool que le CD si l'on considère qu'il vient du passé.
Pourquoi est-elle cool? Sur la gauche, nous pouvons voir la cartouche de la région américaine, le célèbre jeu Zelda, si quelqu'un n'a pas joué - jouer, super. Et si nous démontons cette cartouche, nous y trouverons des microcircuits. Il n'y a pas de disque laser, etc. Habituellement, ces puces ne contiennent que quelques données. De plus, la cartouche coupe directement dans notre système informatique, dans le bus CPU et PPU. Il vous permet de faire des choses incroyables et d'améliorer l'expérience utilisateur.
Il y a un mappeur à bord de la cartouche, il se remplit de la traduction des adresses. Disons que nous avons un gros match. Mais le NES n'a que 32 kilo-octets de mémoire qu'il peut adresser pour le programme. Un jeu, disons, fait 128 kilo-octets. mapper peut, à la volée, pendant l'exécution du programme, remplacer une certaine plage de mémoire par des données complètement nouvelles. Nous pouvons dire dans le programme: chargez-nous niveau 2, et la mémoire sera directement remplacée, presque instantanément.
De plus, il y a des choses amusantes. Par exemple, le mappeur peut fournir des puces qui élargissent la bande son, en ajoutent de nouvelles, etc. Si vous avez joué à Castlevania, écoutez à quoi ressemble le Castlevania de la région japonaise. Il y a un son supplémentaire, ça sonne complètement différent. Dans ce cas, tout est effectué sur le même matériel. Autrement dit, cette idée ressemble davantage à celle lorsque vous avez acheté une carte vidéo, l'avez branchée sur un ordinateur et que vous disposez de fonctionnalités supplémentaires. C'est pareil ici. C'est génial. Mais nous sommes coincés avec des CD.
Passons à la dernière partie - voyons comment fonctionne ce périphérique de sortie d'image. Car si vous voulez faire un émulateur, le programme minimum est de faire un processeur et cette chose pour regarder à quoi ressemblent les images et les jeux vidéo.
Commençons par l'entité de niveau supérieur - l'image elle-même. Il a deux plans. Il y a un premier plan où des entités plus dynamiques sont placées et un arrière-plan où des entités plus statiques comme une scène sont placées.
Vous pouvez voir la division ici. Sur la gauche se trouve le même célèbre jeu Castlevania, donc tout notre voyage vers PPU se déroulera avec Simon Belmont. Avec lui, nous examinerons comment tout fonctionne.
Il y a un arrière-plan, des colonnes, etc. On voit qu'ils sont dessinés en arrière-plan, mais en même temps tous les personnages - Simon lui-même (gauche, marron) et les fantômes - sont déjà dessinés au premier plan. Autrement dit, le premier plan existe pour des entités plus dynamiques et l'arrière-plan existe pour des entités plus statiques.
Une image sur un affichage bitmap se compose de pixels. Les pixels ne sont que des points colorés. À tout le moins, nous avons besoin de couleurs. Le NES a une palette système. Il se compose de 64 couleurs, qui sont malheureusement toutes les couleurs que la NES est capable de reproduire. Mais nous ne pouvons prendre aucune couleur de la palette. Pour les palettes personnalisées, il existe une plage spécifique en mémoire, qui, à son tour, est également divisée en deux de ces sous-plages.
Il existe une gamme d'arrière-plan et de premier plan. Chaque gamme est divisée en quatre palettes de quatre couleurs. Par exemple, l'arrière-plan, la palette zéro se compose de blanc, bleu, rouge. Et la quatrième couleur de chaque palette fait toujours référence à une couleur transparente, ce qui nous permet de créer un pixel transparent.
Cette plage à palettes n'est plus située sur le bus CPU, mais sur le bus PPU. Voyons comment nous pouvons y écrire des données, car nous n'avons pas accès au bus PPU via le bus CPU.
Ici, nous revenons à l'idée d'E / S mappées en mémoire. Il existe des adresses 0x2006 et 0x2007, ce sont des adresses hexadécimales, mais ce ne sont que des nombres. Et nous écrivons comme ça. Puisque nous avons une adresse 16 bits, nous écrivons l'adresse dans le registre d'adresses ox2006 en deux approches de huit bits, puis nous pouvons écrire nos données via l'adresse 0x2007. Une chose tellement drôle. Autrement dit, nous devons effectuer trois opérations pour au moins écrire quelque chose dans la palette.
Excellent. Nous avons une palette, mais nous avons besoin de structures. Les couleurs sont toujours bonnes, mais les bitmaps ont une certaine structure.
Pour les graphiques, il existe deux tableaux de quatre kilo-octets contenant chacun des tuiles. Et toute cette mémoire est une sorte d'atlas. Auparavant, lorsque tout le monde utilisait une image raster, ils créaient un grand atlas, à partir duquel ils sélectionnaient ensuite les images nécessaires à travers l'image de fond par coordonnées. Voici la même idée.
Chaque table a 256 tuiles. Encore une fois, numérologie amusante: exactement 256 vous permet de spécifier un octet, 256 valeurs différentes. Autrement dit, dans un octet, nous pouvons spécifier n'importe quelle tuile dont nous avons besoin. Il s'avère que deux tableaux. Un tableau pour les arrière-plans, un autre pour le premier plan.
Voyons comment ces tuiles sont stockées. C'est drôle ici aussi. Rappelons-nous que nous avons quatre couleurs dans notre palette. Encore une fois, la numérologie: un octet a huit bits et une tuile fait huit par huit. Il s'avère qu'avec un octet, nous pouvons représenter une bande d'une tuile, où chaque bit sera responsable d'une couleur. Et avec huit octets, nous pouvons représenter une tuile complète de huit par huit.
Mais il y a un problème ici. Comme nous l'avons dit, un bit est responsable de la couleur, mais il ne peut représenter que deux valeurs. Les tuiles sont stockées dans deux plans. Il existe un plan du bit le plus significatif et le moins significatif. Pour obtenir la couleur finale, nous combinons les données des deux plans.
Vous pouvez considérer - ici, par exemple, la lettre "I", la partie inférieure, il y a le nombre "3", qui se révèle comme ceci: nous prenons le plan des bits les moins significatifs et les plus significatifs et obtenons le nombre binaire 11, qui sera égal à la décimale 3. Une structure de données aussi amusante.
Contexte
Maintenant, nous pouvons enfin rendre l'arrière-plan!
Il y a une table de noms pour cela. Nous en avons deux, chacun de 960 octets, chaque octet nous renvoie à une tuile spécifique. Autrement dit, l'identificateur de tuile est indiqué dans le tableau précédent. Si nous représentons ces 960 octets sous forme de matrice, nous obtenons un écran de 32 par 30 tuiles. La résolution NES sera de 256 pixels sur 240 pixels.
Excellent. On peut y écrire des carreaux. Mais comme vous l'avez peut-être remarqué, les tuiles n'indiquent pas la palette avec laquelle elles doivent être affichées. Nous pouvons afficher différentes tuiles avec différentes palettes, et nous devons également stocker ces informations quelque part. Malheureusement, nous n'avons que 64 octets par table de noms pour stocker les informations de palette.
Et voici le problème. Si nous divisons le tableau davantage pour qu'il n'y ait que 64 valeurs, nous obtenons des carrés de quatre sur quatre tuiles, qui ressemblent à un carré rouge. Ceci est juste une grande partie de l'écran. Elle serait subordonnée à une palette, sinon pour une mais.
Comme nous nous en souvenons, il y a quatre palettes dans la sous-palette, et nous n'avons besoin que de deux bits pour indiquer celle dont nous avons besoin. Chacun de ces 64 octets copie les informations de la palette pour une grille quatre par quatre. Mais cette grille est toujours divisée en de telles sous-grilles deux par deux. Bien sûr, il y a une limitation: une grille deux par deux est liée à une palette. Ce sont les limites du monde de l'affichage des arrière-plans sur Nintendo. Fait amusant, mais en général, cela n'interfère pas vraiment avec les jeux.
Il y a aussi un défilement. Si nous nous rappelons, par exemple, "Mario" ou Castlevania, alors nous savons: si dans ces jeux le héros se déplace vers la droite, alors le monde semble se dérouler le long de l'écran. Cela se fait par défilement.
Rappelons que nous avons deux tables de noms qui encodent déjà deux écrans. Et quand notre héros bouge, nous ajoutons en quelque sorte des données au tableau des noms qui suit. À la volée, lorsque notre héros se déplace, nous remplissons le tableau des noms. Il s'avère que nous pouvons indiquer à partir de quelle tuile dans la table des noms nous avons besoin pour commencer à afficher les données, et nous les développerons en bandes. Toute l'astuce du défilement consiste à lire les deux tables de noms.
Autrement dit, si nous allons au-delà d'une table de noms horizontalement, alors nous commençons à lire automatiquement une autre, etc. Et n'oubliez pas, encore une fois, de remplir les données.
Au fait, le défilement était une chose assez importante à l'époque. Les premières réalisations de John Carmack ont été dans le domaine du défilement. Regardez cette histoire, c'est assez drôle.
Premier plan
Et le premier plan. Au premier plan, comme nous l'avons dit, il y a des entités dynamiques, et elles sont stockées dans la mémoire des objets et des attributs.
Il y a 256 octets sur lesquels nous pouvons écrire 64 objets, quatre octets par objet. Chaque objet code X et Y, qui est le décalage de pixel sur l'écran. Plus l'adresse et les attributs de la vignette. On peut prioriser l'arrière-plan, voir l'image ci-dessous? Nous pouvons spécifier la palette. La priorité sur l'arrière-plan indique au PPU que l'arrière-plan doit être dessiné au-dessus du sprite. Cela nous permet de placer Simon derrière la statue.
Nous pouvons également faire l'orientation, la tourner au-delà de n'importe quel axe, par exemple, horizontal, vertical, comme la lettre "I" dans l'image. Nous écrivons à peu près de la même manière que la palette: via l'adresse 0x2003, 0x2004.
Enfin, la fin. Comment rendons-nous les objets de premier plan?
L'image se déroule selon des lignes appelées scanlines, c'est un terme de télévision. Avant chaque scanline, nous prenons simplement huit sprites dans la mémoire d'objets et d'attributs. Pas plus de huit, seulement huit sont pris en charge. Il existe également une telle limitation. Nous les affichons simplement ligne par ligne, comme ici, par exemple. Sur la ligne de balayage actuelle, en jaune, nous affichons un nuage, un soleil et un cœur dans une bande. Et le smiley ne s'affiche pas. Mais il est toujours heureux.
Découvrez la super chaîne One Lone Coder . Il y a le processus de programmation lui-même, en particulier - la programmation de l'émulateur NES. Et Nesdev contient toutes les informations sur l'émulation - de quoi il s'agit, etc. Le dernier lien est le code de mon émulateur . Jetez un coup d'œil si vous êtes intéressé. Écrit en TypeScript.
Merci. J'espère que vous en avez profité.