Emulación NES / Famicom / Dandy en tecnologías web. Informe Yandex

La pila TypeScript, Canvas y Web Audio le permite emular sistemas informáticos utilizando tecnologías web. En mi informe, usando el ejemplo del decodificador de NES, conté cómo se organiza la arquitectura de las computadoras: un procesador, un programa, dispositivos periféricos, asignación de E / S a la memoria.





El informe se puede dividir en tres partes:



  1. cómo funciona el procesador 6502 y cómo emularlo usando JavaScript,
  2. cómo funciona el dispositivo de salida de gráficos y cómo los juegos almacenan sus recursos,
  3. cómo se sintetiza el audio usando audio web y cómo se paraleliza en dos flujos usando un orlet de audio.


Intenté dar consejos sobre optimización. Aún así, la emulación es lo importante, a 60 FPS queda poco tiempo para la ejecución del código.



- Hola a todos, mi nombre es Zhenya. Ahora habrá una pequeña charla inusual, el sábado, sobre el proyecto durante muchos sábados. Hablemos de la emulación de sistemas informáticos, que se puede implementar sobre las tecnologías web existentes. De hecho, la web ya es bastante rica en herramientas y puedes hacer cosas absolutamente increíbles. Más concretamente, hablaremos del emulador a todos, probablemente, la famosa consola Dandy de los años 90, que en realidad se llama Nintendo Entertainment System.







Recordemos un poco de historia. Comenzó en 1983 cuando Famicom salió a la luz en Japón. Fue lanzado por Nintendo. En 1985, se lanzó la versión estadounidense, que se llamó Nintendo Entertainment System. En los 90 teníamos la misma región taiwanesa llamada Dandy, pero secretamente, este es un prefijo no oficial. Y el último regalo de hierro de Nintendo fue en 2016, cuando salió la NES mini. Desafortunadamente, no tengo una NES mini. Hay SNES mini, Super Nintendo. Mire qué cosa tan pequeña, y en esta diapositiva puede ver la Ley de Moore en todo su esplendor.



Si miramos 1985 y la relación entre la consola y el joystick, y en 2016, podemos ver cuánto más pequeño se ha vuelto todo, porque las manos de las personas no cambian, el joystick no se puede hacer más pequeño, pero la consola en sí se ha vuelto pequeña.



Como ya hemos notado, existen muchos emuladores. No lo dijimos, pero al menos un funcionario lo notó. Esta cosa, SNES mini o NES mini, no es realmente un decodificador real. Esta es una pieza de hardware que emula la consola. Eso es, de hecho, este es un emulador oficial, pero que viene en una forma de hierro tan divertida.



Pero como sabemos, desde la década de 2000, existen programas que emulan la NES, gracias a los cuales aún podemos disfrutar de juegos de esa época. Y hay muchos emuladores. ¿Por qué otro, especialmente en JavaScript, me preguntas? Cuando hice esto, encontré tres respuestas a esta pregunta para mí.



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


También vi la presentación de Matt Godbold, quien también habló sobre la emulación del procesador que ejecuta la NES. Dijo que es gracioso que estemos emulando algo de tan bajo nivel en un lenguaje de tan alto nivel. No tenemos acceso al hardware, trabajamos indirectamente.







Pasemos a la consideración de qué emularemos, cómo emularemos, etc. Empezaremos con el procesador. La propia NES es icónica. Para Rusia, es comprensible, este es un fenómeno cultural. Pero en Occidente, y en Oriente, en Japón, también fue un fenómeno cultural, porque la consola, de hecho, salvó a toda la industria de los videojuegos domésticos.



El procesador también está instalado en el icónico MOS6502. Cual es su significado? En el momento en que apareció, sus competidores tenían un precio de $ 180 y el MOS6502 tenía un precio de $ 25. Es decir, este procesador lanzó la revolución de la computadora personal. Y aquí tengo dos computadoras. El primero es Apple II, todos sabemos e imaginamos lo significativo que fue este evento para el mundo de las computadoras personales.



También hay una computadora BBC Micro. Era más popular en Gran Bretaña, la BBC es una corporación de televisión británica. Es decir, este procesador trajo las computadoras a las masas, gracias a él ahora somos programadores, desarrolladores front-end.



Echemos un vistazo al programa mínimo. ¿Qué necesitamos para hacer un sistema informático?







La CPU en sí es un dispositivo bastante inútil. Como sabemos, la CPU ejecuta el programa. Pero al menos para que este programa se almacene en algún lugar, se necesita memoria. Y, por supuesto, está incluido en el programa mínimo. Y nuestra memoria consta de celdas de ocho bits, que se denominan bytes.



En JavaScript, podemos usar matrices Uint8Array con tipo para emular esta memoria, es decir, podemos asignar una matriz.



Para que la memoria interactúe con el procesador, hay un bus. El bus permite al procesador direccionar la memoria a través de direcciones. Las direcciones ya no constan de ocho bits, como datos, sino de 16, lo que nos permite direccionar 64 kilobytes de memoria.







Hay un cierto estado en el procesador, hay tres registros: A, X, Y. Un registro es como un almacenamiento de valores intermedios. El tamaño del registro es de un byte u ocho bits. Esto nos dice que el procesador es de ocho bits, opera con datos de ocho bits.



Un ejemplo de uso del registro. Queremos sumar dos números, pero solo hay un bus en la memoria. Resulta que necesitas almacenar el primer número en algún punto intermedio. Lo guardamos en el registro A, podemos tomar el segundo valor de la memoria, agregarlo y el resultado se coloca nuevamente en el registro A.



Funcionalmente, estos registros son bastante independientes, se pueden usar como generales. Pero tienen un significado, como la suma, el resultado se obtiene en el registro A y se toma el valor del primer operando.



O, por ejemplo, abordamos datos. Hablaremos de esto un poco más tarde. Podemos especificar el modo de direccionamiento de compensación y usar el registro X para obtener el valor final.



¿Qué más se incluye en el estado del procesador? Hay un registro de PC que apunta a la dirección del comando actual, ya que la dirección es de dos bytes.



También tenemos el registro de estado, que indica las banderas de estado. Por ejemplo, si restamos dos valores y obtenemos negativos, entonces se enciende un cierto bit en el registro de banderas.



Finalmente, está SP, un puntero a la pila. La pila es solo memoria ordinaria, no está separada de todo lo demás, de todos los demás programas. Simplemente hay una instrucción del procesador que controla este puntero SP. Así es como se implementa la pila. Luego, analizamos una gran idea informática que conduce a soluciones tan interesantes.







Ahora sabemos que hay un procesador, una memoria, un estado en el procesador. Veamos cuál es nuestro programa. Esta es una secuencia de bytes. Ni siquiera tiene que ser consistente. El programa en sí puede ubicarse en diferentes partes de la memoria.



Podemos imaginar un programa, tengo un fragmento de código aquí: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. Este es un programa real 6502. Cada byte de este programa, cada dígito en esta matriz es entidad como código de operación. Opcode - código de operación. “Entonces, nuevamente, un número ordinario.



Por ejemplo, hay un código de operación 169. Codifica dos cosas en sí mismo: primero, una instrucción. Cuando se ejecuta, la instrucción cambia el estado del procesador, la memoria, etc., es decir, el estado del sistema. Por ejemplo, sumamos dos números, el resultado aparece en el registro A. Esta es una instrucción de ejemplo. También tenemos una instrucción LDA, que consideraremos con más detalle. Carga un valor de la memoria en el registro A.



Lo segundo que codifica el código de operación es el modo de direccionamiento. Da instrucciones sobre dónde obtener sus datos. Por ejemplo, si este es el modo de direccionamiento IMM, entonces dice: tome los datos que están en la celda junto al contador del programa actual. También veremos cómo funciona este modo y cómo se puede implementar en JavaScript.



Ese es el programa. Aparte de estos bytes, todo es muy similar a JavaScript, solo que en un nivel inferior.







Si recuerda de lo que estaba hablando, puede haber una paradoja divertida. Resulta que nosotros almacenamos el programa en la memoria y también los datos. Uno podría hacerse esta pregunta: ¿puede un programa actuar como datos? La respuesta es sí. Podemos cambiar desde el propio programa en el momento de la ejecución de este programa.



O otra pregunta: ¿pueden los datos ser un programa? Si tambien. El procesador no importa. Simplemente, como un molino, muele los bytes que se le envían y sigue las instrucciones. Algo paradójico. Si lo piensas, es súper inseguro. Puede comenzar a ejecutar un programa que solo contenga datos en la pila, etc. Pero la ventaja es que es muy fácil. No es necesario hacer circuitos complicados.



Esta es la primera gran idea que nos encontramos hoy. Se llama arquitectura de von Neumann. Pero en realidad había muchos coautores allí.



Aquí se ilustra. Hay el programa 1, código de operación 169, seguido de 10, algunos datos. Bueno. Este programa también se puede ver así: 169 son datos y 10 es un código de operación. Este sería un programa legal para 6502. Todo este programa, nuevamente, puede considerarse como datos.



Si tenemos un compilador, podemos construir algo, ponerlo en esta pieza de memoria y será muy divertido.



Echemos un vistazo a la primera parte de nuestro programa: instrucciones.







6502 proporciona acceso a 73 instrucciones, incluida la aritmética: suma, resta. Sin multiplicación ni división, lo siento. Hay operaciones de bits, se trata de manipular bits en palabras de ocho bits.



Hay saltos que están prohibidos en nuestra interfaz: la declaración de salto, que simplemente transfiere el contador del programa a alguna parte del código. Esto está prohibido en la programación, pero si se trata de un nivel bajo, esta es la única forma de hacer ramificaciones. Hay operaciones para la pila, etc. Están agrupadas. Sí, tenemos 73 instrucciones, pero si observa los grupos y lo que hacen, realmente no hay muchos y todos son bastante similares.



Volvamos a la instrucción LDA. Como dijimos, esto es "cargar el valor de la memoria en el registro A". Así de simple puede ser en JavaScript. En la entrada está la dirección que nos proporciona el modo de direccionamiento. Cambiamos el estado en el interior, decimos que this._a es igual al valor leído de la memoria.



Todavía necesitamos establecer estos campos de dos bits en el registro de estado: una bandera cero y una bandera negativa. Aquí hay muchas cosas bit a bit. Pero si crea un emulador, se convierte en una segunda naturaleza hacer estos OR, negativos, etc. Lo único curioso aquí es que hay un% 256 en la segunda rama. Nos remite, nuevamente, a la naturaleza de nuestro amado lenguaje JavaScript, que no tiene valores escritos. El valor que ponemos en Status puede ir más allá de 256, que caben en un byte. Tenemos que lidiar con esos trucos.



Ahora veamos la parte final de nuestro código de operación, el modo de direccionamiento.







Tenemos 12 modos de direccionamiento. Como dijimos antes, nos permiten obtener e indicar para la instrucción de dónde obtener los datos.



Echemos un vistazo a tres cosas. El último es ABS, modo de direccionamiento absoluto, empecemos con él, me disculpo por un poco de vergüenza. Hace algo como esto. Le damos la dirección completa, 16 bits, como entrada. Nos obtiene el valor de esta celda de memoria. En ensamblador, en la segunda columna, puede ver cómo se ve: LDA $ ccbb. ccbb es un número hexadecimal, un número ordinario, simplemente escrito en una notación diferente. Si se siente incómodo aquí, recuerde que esto es solo un número.



En la tercera columna, puede ver cómo se ve en código de máquina. Delante está el código de operación: 173, resaltado en azul. Y 187 y 204 ya son datos de direcciones. Pero como estamos operando con valores de ocho bits, necesitamos dos ubicaciones de memoria para escribir la dirección.



También me olvidé de decir que el código de operación se ejecuta durante algún tiempo en la CPU, tiene un cierto costo. LDA con direccionamiento absoluto toma cuatro ciclos de CPU.



Aquí ya puede comprender por qué se necesitan tantos modos de direccionamiento. Considere el siguiente modo de direccionamiento, ZP0. Este es el modo de direccionamiento de página cero. Y la página cero son los primeros 256 bytes asignados en la memoria. Estas son direcciones de cero a 255.



En ensamblador, nuevamente, LDA * 10. ¿Qué hace este modo de direccionamiento? Él dice: vaya a la página cero, aquí en estos primeros 256 bytes, con tal o cual desplazamiento. en este caso 10 y tome el valor de allí. Aquí ya notamos una diferencia significativa entre los modos de direccionamiento.



En el caso del direccionamiento absoluto, necesitábamos, en primer lugar, tres bytes para escribir dicho programa. En segundo lugar, necesitábamos cuatro ciclos de CPU. Y en el modo de direccionamiento ZP0, solo tomó tres ciclos de CPU y dos bytes. Pero sí, hemos perdido flexibilidad. Es decir, solo podemos poner nuestros datos en la primera página, esta.



El modo de direccionamiento final IMM dice: tome datos de la celda junto al código de operación. Este LDA # 10 en ensamblador hace eso. Y resulta que el programa se parece a [169, 10]. Ya requiere dos ciclos de CPU. Pero aquí está claro que también perdemos flexibilidad y necesitamos que el código de operación esté al lado de los datos.



Implementar esto en JavaScript es fácil. Aquí tienes un código de muestra. Hay una direccion. Este es el direccionamiento IMM, que toma datos del contador del programa. Simplemente decimos que nuestra dirección es un contador de programa y la incrementamos en uno para que la próxima vez que se ejecute el programa, salte a la siguiente instrucción.



Aquí hay algo gracioso. Ahora podemos leer código de máquina como desarrolladores frontend. E incluso sabemos cómo ver lo que está escrito allí en el ensamblador.







Ya sabemos todo lo que necesitamos en principio. Hay un programa, consta de bytes. Cada byte es un código de operación, cada código de operación es una instrucción, etc. Veamos cómo se ejecuta nuestro programa. Y se ejecuta solo en estos ciclos de CPU.



¿Cómo se puede hacer tal código? Ejemplo. Necesitamos leer el código de operación del contador de software, luego simplemente aumentarlo en uno. Ahora necesitamos decodificar este código de operación en una instrucción y en modo de direccionamiento. Si lo piensas bien, el código de operación es un número primo, 169. Y en un byte solo tenemos 256 números. Podemos hacer una matriz con 256 valores. Cada elemento de esta matriz simplemente nos referirá a qué instrucción usar, qué modo de direccionamiento se necesita y cuántos ciclos tomará. Es decir, es super simple. Y la matriz que tengo está en el estado del procesador.



A continuación, simplemente realizamos la función del modo de direccionamiento en la línea 36, ​​que nos da la dirección, y le damos instrucciones.



Lo último que debemos hacer es lidiar con los bucles. opcodeResolver devuelve el número de ciclos, los escribimos en la variable FixedCycles. Observamos cada ciclo del procesador: si quedan cero ciclos, entonces podemos ejecutar el siguiente comando, si es mayor que cero, simplemente lo disminuimos en uno. Y eso es todo, super simple. Así es como se ejecuta el programa en 6502.







Pero como ya dijimos, el programa puede estar en diferentes partes de la memoria, en diferentes proporciones, etc. ¿Cómo puede un procesador entender por dónde empezar a ejecutar este programa? Necesitamos un int main como este del mundo C.



De hecho, todo es sencillo. El procesador tiene un procedimiento para restablecer su estado. En este procedimiento, tomamos la dirección del comando inicial de la dirección 0xfffxc. 0xfffxc es nuevamente un número hexadecimal. Si se siente incómodo, puntúe, este es el número habitual. Así es como se escriben en JavaScript, a través de 0x.



Necesitamos leer dos bytes de la dirección, la dirección es de 16 bits. Leemos los bytes bajos de esta dirección, los bytes altos de la siguiente dirección. Y luego agregamos este caso con tanta magia de operaciones de bits. Además, restablecer el estado del procesador también restablece el valor en los registros: registro A, X, Y, puntero a la pila, estado. El reinicio toma ocho ciclos. Esa es la cosa.







Ya lo sabemos todo ahora. Para ser honesto, fue un poco difícil para mí escribir todo esto, porque no entendía en absoluto cómo probarlo. Estamos escribiendo una computadora completa que puede ejecutar cualquier programa que se haya creado para ella. ¿Cómo entender que nos estamos moviendo correctamente?



¡Hay una manera magnífica y maravillosa! Tomamos dos CPU. El primero es el que hacemos, el segundo es la CPU de referencia, sabemos a ciencia cierta que funciona bien. Por ejemplo, hay un emulador para NES, el nintendulator, que se considera un punto de referencia para las CPU.



Tomamos un determinado programa de prueba, lo ejecutamos en la CPU de referencia y escribimos el estado del procesador en el registro de estado para cada comando. Luego tomamos este programa y lo ejecutamos en nuestra CPU. Y cada estado después de cada comando se compara con este registro. ¡Súper idea!



Por supuesto, no necesitamos una referencia de CPU. Solo necesitamos un registro de ejecución del programa. Este registro se puede encontrar en Nesdev. De hecho, un emulador de procesador se puede escribir, no sé, en un par de días el fin de semana, ¡es simplemente magnífico!



Y eso es todo. Tomamos el registro, comparamos el estado y tenemos una prueba interactiva. Ejecutamos el primer comando, no está implementado en el procesador que estamos desarrollando. Lo implementamos, vamos a la siguiente línea del registro y lo implementamos nuevamente. ¡Súper rápido! Le permite moverse rápidamente.



Arquitectura NES



Ahora tenemos una CPU, que es esencialmente el corazón de nuestra computadora. Y podemos ver de qué está hecha la arquitectura de la propia NES y cómo se fabrican estos complejos sistemas informáticos compuestos. Porque si lo piensas bien, hay una CPU, hay memoria. Podemos recibir valores, grabar, etc.







Pero en la NES, en cualquier set-top box, también hay una pantalla, dispositivos de sonido, etc. Necesitamos aprender a trabajar con periféricos. Ni siquiera necesitas aprender nada nuevo para esto, el concepto de nuestro autobús es suficiente. Esta es probablemente la segunda idea tan brillante, un descubrimiento brillante que hice por mí mismo en el proceso de escribir un emulador.



Imaginemos que tomamos nuestra memoria, que era de 64 kilobytes, y la dividimos en dos rangos de 32 kilobytes. En el rango inferior habrá un dispositivo determinado, que es una matriz de bombillas, como en la imagen con esta placa.



Digamos que al escribir en este rango junior de 32 kilobytes, la luz se encenderá o se apagará. Si escribimos allí el valor 1, la luz se enciende, si 0 - se apaga. Al mismo tiempo, podemos leer el valor y comprender el estado del sistema, comprender qué imagen se muestra en esta pantalla.



Nuevamente, en el rango alto de direcciones colocamos la memoria ordinaria en la que se encuentra el programa, porque necesitamos una dirección en el rango alto durante el procedimiento de reinicio.



En realidad, esta es una idea súper genial. Para interactuar con los periféricos, no se necesitan comandos adicionales, etc. Simplemente escribimos en una buena memoria antigua, como antes. Pero al mismo tiempo, la memoria ya puede ser dispositivos adicionales.







Ahora estamos completamente preparados para echar un vistazo a la arquitectura de NES. Tenemos una CPU y su bus, como es habitual. Hay dos kilobytes adicionales de memoria. Hay una APU, un dispositivo de salida de sonido. Desafortunadamente, ahora no lo consideraremos, pero todo es genial allí también. Y hay un cartucho. Se coloca en el rango alto y proporciona datos del programa. También proporciona estos gráficos, ahora los consideraremos. Lo último en el bus de la CPU es una PPU, una unidad de procesamiento de imágenes, como una proto-tarjeta de video. Si deseaba aprender a trabajar con tarjetas de video, ahora incluso vamos a aprender a implementar una.



El PPU también tiene su propio bus, en el que se desplazan las tablas de nombres, las paletas y los datos gráficos. Pero los datos gráficos provienen del cartucho. Y luego está la memoria del objeto. Esta es la arquitectura.







Veamos qué es un cartucho. Esta es una idea mucho más genial que el CD si se considera que es del pasado.



¿Por qué es genial? A la izquierda podemos ver el cartucho de la región americana, el famoso juego Zelda, si alguien no lo ha jugado - play, super. Y si desmontamos este cartucho, encontraremos microcircuitos en él. No hay disco láser, etc. Por lo general, estos chips solo contienen algunos datos. Además, el cartucho se conecta directamente a nuestro sistema informático, a la CPU y al bus PPU. Te permite hacer cosas increíbles y mejorar la experiencia del usuario.



Hay un mapeador a bordo del cartucho, se llena con la traducción de direcciones. Digamos que tenemos un gran juego. Pero la NES tiene solo 32 kilobytes de memoria que puede asignar al programa. Un juego, digamos, tiene 128 kilobytes. mapper puede, sobre la marcha, durante la ejecución del programa, reemplazar un cierto rango de memoria con datos completamente nuevos. Podemos decir en el programa: cárganos nivel 2, y la memoria será reemplazada directamente, casi al instante.



Además, hay cosas divertidas. Por ejemplo, el mapeador puede proporcionar chips que amplíen la banda sonora, agreguen nuevas, etc. Si has jugado a Castlevania, escucha cómo suena el Castlevania de la región japonesa. Hay un sonido adicional, suena completamente diferente. En este caso, todo se realiza en el mismo hardware. Es decir, esta idea es más parecida a la de cuando compró una tarjeta de video, la conectó a una computadora y tiene una funcionalidad adicional. Es lo mismo aqui. Eso es genial. Pero estamos atrapados con los CD.







Pasemos a la parte final: veamos cómo funciona este dispositivo de salida de imágenes. Porque si quieres hacer un emulador, el programa mínimo es hacer un procesador y eso para ver cómo se ven las imágenes y los videojuegos.



Comencemos con la entidad de nivel superior: la imagen en sí. Tiene dos planes. Hay un primer plano donde se colocan entidades más dinámicas y un fondo donde se colocan entidades más estáticas como una escena.



Puedes ver la división aquí. A la izquierda está el mismo juego famoso de Castlevania, por lo que todo nuestro viaje a PPU ocurrirá con Simon Belmont. Junto con él, consideraremos cómo funciona todo.



Hay un fondo, columnas, etc. Vemos que están dibujadas en el fondo, pero al mismo tiempo todos los personajes -el mismo Simon (izquierda, marrón) y fantasmas- ya están dibujados en primer plano. Es decir, el primer plano existe para entidades más dinámicas y el fondo existe para entidades más estáticas.







Una imagen en una pantalla de mapa de bits consta de píxeles. Los píxeles son solo puntos de colores. Por lo menos, necesitamos colores. La NES tiene una paleta de sistema. Consta de 64 colores, que desafortunadamente son todos los colores que la NES es capaz de reproducir. Pero no podemos sacar ningún color de la paleta. Para las paletas personalizadas, hay un rango específico en la memoria, que, a su vez, también se divide en dos subrangos.



Hay una variedad de antecedentes y primeros planos. Cada gama se divide en cuatro paletas de cuatro colores. Por ejemplo, la paleta de fondo, cero consta de blanco, azul, rojo. Y el cuarto color en cada paleta siempre se refiere a un color transparente, lo que nos permite hacer un píxel transparente.







Este rango con paletas ya no se encuentra en el bus de la CPU, sino en el bus PPU. Veamos cómo podemos escribir datos allí, porque no tenemos acceso al bus PPU a través del bus de la CPU.



Aquí volvemos de nuevo a la idea de E / S mapeadas en memoria. Hay direcciones 0x2006 y 0x2007, estas son direcciones hexadecimales, pero son solo números. Y escribimos así. Como nuestra dirección es de 16 bits, escribimos la dirección en el registro de direcciones ox2006 en dos enfoques de ocho bits y luego podemos escribir nuestros datos a través de la dirección 0x2007. Qué cosa tan divertida. Es decir, de hecho, necesitamos realizar tres operaciones para al menos escribir algo en la paleta.







Excelente. Tenemos una paleta, pero necesitamos estructuras. Los colores siempre son buenos, pero los mapas de bits tienen cierta estructura.



Para los gráficos, hay dos tablas de cuatro kilobytes cada una que contiene mosaicos. Y todo este recuerdo es una especie de atlas. Anteriormente, cuando todos usaban una imagen rasterizada, hacían un gran atlas, del cual luego seleccionaban las imágenes necesarias a través de la imagen de fondo por coordenadas. Esta es la misma idea.



Cada mesa tiene 256 fichas. Nuevamente, numerología divertida: exactamente 256 le permite especificar un byte, 256 valores diferentes. Es decir, en un byte podemos especificar cualquier mosaico que necesitemos. Resulta dos tablas. Una tabla para fondos, otra para primer plano.







Veamos cómo se almacenan estos mosaicos. Aquí también es algo gracioso. Recordemos que tenemos cuatro colores en nuestra paleta. Numerología de nuevo: un byte tiene ocho bits y un mosaico de ocho por ocho. Resulta que con un byte podemos representar una franja de un mosaico, donde cada bit será responsable de algún color. Y con ocho bytes, podemos representar un mosaico completo de ocho por ocho.



Pero hay un problema aquí. Como dijimos, un bit es responsable del color, pero solo puede representar dos valores. Los mosaicos se almacenan en dos planos. Hay un plano del bit más significativo y el menos significativo. Para obtener el color final, combinamos datos de ambos planos.



Puede considerar: aquí, por ejemplo, la letra "I", la parte inferior, está el número "3", que resulta así: tomamos el plano de los bits menos significativos y más significativos y obtenemos el número binario 11, que será igual al decimal 3. Una estructura de datos tan divertida.



Antecedentes



¡Ahora finalmente podemos renderizar el fondo!







Hay una tabla de nombres para ello. Tenemos dos de ellos, cada uno de 960 bytes, cada byte nos remite a un mosaico específico. Es decir, el identificador de mosaico se indica en la tabla anterior. Si representamos estos 960 bytes como una matriz, obtenemos una pantalla de mosaico de 32 por 30. La resolución de NES será de 256 píxeles por 240 píxeles.



Excelente. Podemos escribir mosaicos allí. Pero como habrás notado, los mosaicos no indican la paleta con la que deben mostrarse. Podemos mostrar diferentes mosaicos con diferentes paletas, y también necesitamos almacenar esta información en algún lugar. Desafortunadamente, solo tenemos 64 bytes por tabla de nombres para almacenar información de paleta.



Y aquí es donde surge el problema. Si dividimos aún más la tabla de modo que solo haya 64 valores, obtenemos cuadrados de cuatro por cuatro mosaicos, que se ven como un cuadrado rojo. Es solo una gran parte de la pantalla. Ella estaría subordinada a una paleta, si no por una, sino.



Como recordamos, hay cuatro paletas en la subpaleta, y solo necesitamos dos bits para indicar la que necesitamos. Cada uno de estos 64 bytes copia la información de la paleta para una cuadrícula de cuatro por cuatro. Pero esta cuadrícula todavía está dividida en subcuadrículas de dos en dos. Por supuesto, aquí hay una limitación: una cuadrícula de dos por dos está vinculada a una paleta. Estas son las limitaciones en el mundo de la visualización de fondos en Nintendo. Dato curioso, pero en general no interfiere con los juegos.







También hay desplazamiento. Si recordamos, por ejemplo, "Mario" o Castlevania, entonces sabemos: si en estos juegos el héroe se mueve hacia la derecha, entonces el mundo parece desarrollarse a lo largo de la pantalla. Esto se hace desplazándose.



Recuerde que tenemos dos tablas de nombres que ya codifican dos pantallas. Y cuando nuestro héroe se mueve, agregamos datos a la tabla de nombres que sigue. Sobre la marcha, cuando nuestro héroe se mueve, completamos la tabla de nombres. Resulta que podemos indicar desde qué mosaico en la tabla de nombres necesitamos comenzar a mostrar datos, y lo expandiremos en tiras. Todo el truco del desplazamiento es leer de las dos tablas de nombres.



Es decir, si vamos más allá de una tabla de nombres horizontalmente, entonces comenzamos a leer automáticamente desde otra, etc. Y no olvidemos, nuevamente, completar los datos.



Por cierto, el desplazamiento era algo muy importante en ese momento. Los primeros logros de John Carmack fueron en el campo del desplazamiento. Mira esta historia, es bastante divertida.



Primer plano



Y el primer plano. En primer plano, como dijimos, hay entidades dinámicas, y están almacenadas en la memoria de objetos y atributos.







Hay 256 bytes en los que podemos escribir 64 objetos, cuatro bytes por objeto. Cada objeto codifica X e Y, que es el desplazamiento de píxeles en la pantalla. Además de la dirección y los atributos del mosaico. Podemos priorizar el fondo, ¿ves la imagen de abajo? Podemos especificar la paleta. La prioridad sobre el fondo le dice al PPU que el fondo debe dibujarse encima del sprite. Esto nos permite colocar a Simón detrás de la estatua.



También podemos hacer la orientación, girarla más allá de cualquier eje, por ejemplo, horizontal, vertical, como la letra "I" de la imagen. Escribimos aproximadamente de la misma forma que la paleta: a través de la dirección 0x2003, 0x2004.



Finalmente, la final. ¿Cómo renderizamos objetos en primer plano?







La imagen se desarrolla a lo largo de líneas llamadas líneas de exploración, este es un término de televisión. Antes de cada línea de exploración, simplemente tomamos ocho sprites de la memoria de objetos y atributos. No más de ocho, solo ocho son compatibles. También existe tal limitación. Los mostramos línea por línea, como aquí, por ejemplo. En la línea de exploración actual, en amarillo mostramos una nube, un sol y un corazón en una raya. Y el emoticón no se muestra. Pero todavía está feliz.



Echa un vistazo al supercanal de One Lone Coder . Está el proceso de programación en sí, en particular: programar el emulador de NES. Y Nesdev contiene toda la información sobre la emulación, en qué consiste, etc. El enlace final es el código de mi emulador . Eche un vistazo si está interesado. Escrito en TypeScript.



Gracias. Espero que lo hayas disfrutado.



All Articles