Ingeniería inversa de arcade: grabar Michael Jordan en NBA Jam


El verano pasado fui invitado a una fiesta en Sunnyvale. Resultó que los propietarios en el garaje tienen una máquina arcade NBA JAM Tournament Edition para cuatro jugadores. A pesar de que el juego ya tiene más de 25 años (fue lanzado en 1993), todavía es muy interesante jugarlo, especialmente para los fanáticos entusiastas.

Me sorprendió la lista de jugadores de los Chicago Bulls que no incluía a Michael Jordan. Según las fuentes [1] , MJ recibió su propia licencia y no formó parte del acuerdo que Midway hizo con la NBA.

Después de preguntarle al propietario de la máquina, supe que los piratas informáticos lanzaron un mod para SNES "NBA Jam 2K17", que permite que jueguen nuevos jugadores y MJ, pero nadie estaba analizando cómo funcionaba la versión arcade. Por lo tanto, definitivamente tuve que mirar dentro.

Antecedentes


La historia del NBA Jam comienza no con el baloncesto, sino con Jean-Claude Van Damme. Casi al mismo tiempo que se lanzó Universal Soldier, Midway Games desarrolló tecnología para manipular sprites fotorrealistas, digitalizados y grandes que conservan un parecido con actores reales. Fue un gran avance tecnológico: animaciones con 60 cuadros por segundo, sprites sin precedentes de 100x100 píxeles de tamaño, cada uno de los cuales tenía su propia paleta de 256 colores.

La compañía utilizó esta tecnología con gran éxito en el popular juego de disparos "Terminator 2: Judgment Day" [2] , pero no pudo adquirir una licencia para "Universal Soldier" (las condiciones financieras de JCVD ​​eran inaceptables para Midway [3] ). Cuando las negociaciones terminaron en un fracaso, Midway cambió de rumbo y comenzó a desarrollar un mega juego de lucha de Capcom de 1991 llamado Street Fighter II: The World Warrior.

Se formó un equipo de cuatro (Ed Boone escribió el código, John Tobias estuvo involucrado en el arte y las secuencias de comandos, John Vogel dibujó gráficos y Dan Forden era un ingeniero de sonido). Después de un año de arduo trabajo [4] Midway lanzó Mortal Kombat en 1992.

El estilo visual era muy diferente del pixel art habitual, y el diseño del juego era, por decir lo menos, "controvertido". El juego con litros de sangre en la pantalla y empujones increíblemente crueles, la "fatalidad" se convirtió instantáneamente en un éxito mundial y ganó casi mil millones de dólares en un año [5] .


SF2: 384 × 224 con 4.096 colores.


MK: 400 × 254 con 32.768 colores.

Dato interesante: como en el modo VGA 0x13 en PC, en estos juegos los píxeles no eran cuadrados. Aunque el búfer de cuadros de Mortal Kombat tiene un tamaño de 400 × 254, se extiende a una pantalla CRT de relación 4: 3, proporcionando una resolución de 400 × 300 [6]

Equipo de unidad T intermedia


El hardware desarrollado por Midway para Mortal Kombat resultó ser muy bueno. Tan bueno que le dieron su propio nombre T-Unit y lo reutilizaron en otros juegos.

  • Mortal Kombat.
  • Mortal Kombat II.
  • NBA Jam.
  • NBA Jam Tournament Edition.
  • Juez Dredd (no ha sido puesto en libertad).

La unidad T consta de dos tableros. La mayoría de ellos se ocupa de la lógica y los gráficos del juego.


Placa de procesador NBA JAM TE Edition (aproximadamente 40x40 cm, o 15 pulgadas).


La otra placa es menos complicada, pero también capaz de mucho. Está diseñado para audio, pero puede reproducir no solo música usando síntesis FM, sino también sonido digital.

La tarjeta de sonido está conectada a una fuente de alimentación y una tarjeta gráfica instalada en la parte posterior. Presta atención al enorme radiador ubicado en la esquina superior izquierda.

Juntas, estas dos placas contienen más de doscientos chips, resistencias y EPROM. Entender todo esto solo sobre la base de los números de serie llevaría mucho tiempo. Pero, sorprendentemente, a veces en dispositivos de los años 90 se descubre accidentalmente documentación. Y en el caso de NBA Jam, ella era simplemente genial.

Arquitectura Midway T-Unit


Buscando datos, me encontré con un NBA Jam Kit. El nivel de detalle de este documento es sorprendente [7] . Entre otras cosas, logré encontrar una descripción detallada de las conexiones de cableado, incluidas las EPROM y los chips.


La información del documento nos permitió dibujar un diagrama de los tableros y determinar la función de cada parte. Para ayudar en la búsqueda de componentes, el tablero tiene coordenadas con un comienzo en la esquina inferior derecha (UA0), que aumenta a la esquina superior izquierda (UJ26).


El corazón de la placa principal es Texas Instrument TMS34010 (UB21) con una frecuencia de 50 MHz y con un código de 1 mebibyte en EPROMs y 512 kibibytes DRAM [8] . 34010 es un chip de 32 bits con un bus de 16 bits, que tiene instrucciones gráficas tan notables como PIXT y PIXBLT [9] . A principios de los 90, este chip se usó en varias tarjetas de aceleración de hardware [10] , y pensé que maneja una cantidad considerable de efectos gráficos. Sorprendentemente, solo se ocupa de la lógica del juego y no dibuja nada.

De hecho, el chip UE13 llamado "DMA2" resultó ser un monstruo gráfico. Según los diagramas de la documentación, tiene un impresionante (en ese momento) bus de datos de 32 bits y bus de direcciones de 32 bits, por lo que se convirtió en el chip más grande de la placa. Este circuito integrado especializado (ASIC) es capaz de muchas operaciones gráficas, que analizaré a continuación.

Todos los chips (RAM del sistema, EPROM GFX, SDRAM de paleta, código, bancos de video) se asignan a un espacio de direcciones de 32 bits y se conectan al mismo bus. No pude encontrar ninguna información sobre el protocolo del bus, por lo que si sabe algo al respecto, escriba al correo electrónico.

Preste atención a un truco: un componente EPROM (marcado en azul) se usa para crear otro sistema de almacenamiento (y ahorrar dinero). Estas EPROM de 512 kb tienen pines de dirección de 32 bits y pines de datos de 8 bits. Para 34010, que requiere un bus de datos de 16 bits, dos EPROM (J12 y G12) están conectadas con una doble alternancia de direcciones, creando una memoria de 1 mebibyte. Del mismo modo, los recursos gráficos están conectados con una alternancia cuádruple de direcciones para formar una dirección de 32 bits con un sistema de almacenamiento de 32 bits que contiene 8 mebibytes.

Aunque en este artículo consideraré principalmente la tubería de gráficos, no puedo resistir la tentación y, por lo tanto, hablaré brevemente sobre el sistema de audio.


El diagrama de la tarjeta de sonido muestra el Motorola 6809 (U4 con una frecuencia de 2 MHz), que recibe instrucciones de una EPROM (U3) para controlar la música y los efectos de sonido.

El chip de síntesis FM 2151 de Yamaha (3.5 MHz) genera música directamente a partir de las instrucciones recibidas de 6809 (la música usa un ancho de banda bastante pequeño).

OKI6295 (1 MHz) es responsable de reproducir audio digital en formato ADPCM (por ejemplo, el legendario "Boomshakalaka" [11] Tim Kittsrow).

Tenga en cuenta que en la placa principal, la misma EPROM 32a / 8d azul de 512 kbytes se usa en un sistema de 16 bits con doble intercalación de direcciones para almacenar voces digitalizadas, pero para las instrucciones de 8 bits, los datos / direcciones de Motorola 6809 no se entrelazan.

Vida del marco


Toda la pantalla NBA Jam está indexada en una paleta de 16 bits. Los colores se almacenan en formato xRGB 1555 en una paleta de 64 kbytes. La paleta se divide en 128 bloques (256 * 16 bits) de 512 bytes. Los sprites almacenados en EPROM están marcados como "GFX". Cada sprite tiene su propia paleta de colores de hasta 256x16 bits. Un sprite a menudo usa un bloque de paleta completo, pero nunca más de uno. Se transmite una señal CRT al monitor utilizando RAMDAC, que para cada píxel lee el índice de los bancos de DRAM de video y realiza una búsqueda de color en la paleta.

La vida de cada fotograma de un video de NBA Jam procede de la siguiente manera:

  1. La lógica del juego consiste en un flujo de instrucciones de 16 bits transmitidas desde J12 / G12 a 34010.
  2. 34010 lee la entrada del jugador, calcula el estado del juego y luego dibuja una pantalla.
  3. Para dibujar en la pantalla, 34010 primero encuentra un bloque no utilizado en la paleta y escribe allí la paleta de sprites (las paletas de sprites se almacenan junto con las instrucciones 34010 en J12 / G12).
  4. 34010 realiza una solicitud a DMA2, que incluye la dirección y el tamaño del sprite, el bloque de paleta de 8 bits utilizado, el truncamiento, el escalado, el método de procesamiento de píxeles transparentes, etc.
  5. DMA2 lee índices de sprites de 8 bits del chip ROM GFX J14-G23, combina este valor con el índice de un bloque de paleta de 8 bits y escribe un índice de 16 bits en bancos de video. DRAM2 puede considerarse un blitter que lee valores de 8 bits de GFX EPROM y escribe valores de 16 bits en bancos de video
  6. Los pasos 3-5 se repiten hasta que se completen todas las solicitudes para dibujar sprites.
  7. Cuando se trata de actualizar la pantalla, RAMDAC convierte los datos en los bancos de video en una señal que un monitor CRT puede entender. Para que el ancho de banda sea suficiente para convertir el índice de 16 bits a RGB de 16 bits, la paleta se almacena en una SRAM extremadamente costosa y extremadamente rápida.


Un hecho interesante: el firmware flash EPROM no es un proceso tan simple. Antes de escribir en el chip, debe borrar completamente todo su contenido.

Para hacer esto, el chip debe irradiarse con luz UV. Primero debe despegar la etiqueta de la parte superior de la EPROM para abrir su diagrama. Luego, la EPROM se coloca en un dispositivo de borrador especial en el que hay una lámpara UV.

Después de 20 minutos, la EPROM se llenará con ceros y estará lista para grabar.

Documentación MAME


Después de descubrir el equipo, me di cuenta de qué conjunto de EPROM se podía escribir a Michael Jordan (la paleta se almacena en las EPROM de código y los índices en las EPROM de GFX). Sin embargo, todavía no sabía la ubicación exacta o el formato utilizado.

Falta la documentación encontrada en MAME.

En caso de que no sepa cómo funciona este increíble emulador, lo explicaré brevemente. MAME se basa en el concepto de "controladores", que son una imitación de la placa. Cada controlador está compuesto por componentes que imitan (generalmente) cada chip. En el caso de Midway T-Unit, estamos interesados ​​en los siguientes archivos:

  mame / incluye / midtunit.h
 mame / src / mame / video / midtunit.cpp
 mame / src / mame / drivers / midtunit.cpp
 mame / src / mame / machine / midtunit.cpp
 cpu / tms34010 / tms34010.h 

Si observa drivers / midtunit.cpp, veremos que cada chip de memoria es parte de un único espacio de direcciones de 32 bits. Se puede ver en el código fuente del controlador que la paleta comienza en 0x01800000, gfxrom comienza en 0x02000000 y el chip DMA2 comienza en 0x01a80000. Para seguir la ruta de datos, debemos seguir las funciones de C ++ ejecutadas cuando el objeto de la operación de lectura o escritura es la dirección de memoria.

void midtunit_state::main_map(address_map &map) { map.unmap_value_high(); map(0x00000000, 0x003fffff).rw(m_video, FUNC(midtunit_vram_r), FUNC(midtunit_vram_w)); map(0x01000000, 0x013fffff).ram(); map(0x01400000, 0x0141ffff).rw(FUNC(midtunit_cmos_r), FUNC(midtunit_cmos_w)).share("nvram"); map(0x01480000, 0x014fffff).w(FUNC(midtunit_cmos_enable_w)); map(0x01600000, 0x0160000f).portr("IN0"); map(0x01600010, 0x0160001f).portr("IN1"); map(0x01600020, 0x0160002f).portr("IN2"); map(0x01600030, 0x0160003f).portr("DSW"); map(0x01800000, 0x0187ffff).ram().w(m_palette, FUNC(write16)).share("palette"); map(0x01a80000, 0x01a800ff).rw(m_video, FUNC(midtunit_dma_r), FUNC(midtunit_dma_w)); map(0x01b00000, 0x01b0001f).w(m_video, FUNC(midtunit_control_w)); map(0x01d00000, 0x01d0001f).r(FUNC(midtunit_sound_state_r)); map(0x01d01020, 0x01d0103f).rw(FUNC(midtunit_sound_r), FUNC(midtunit_sound_w)); map(0x01d81060, 0x01d8107f).w("watchdog", FUNC(watchdog_timer_device::reset16_w)); map(0x01f00000, 0x01f0001f).w(m_video, FUNC(midtunit_control_w)); map(0x02000000, 0x07ffffff).r(m_video, FUNC(midtunit_gfxrom_r)).share("gfxrom"); map(0x1f800000, 0x1fffffff).rom().region("maincpu", 0); /* mirror used by MK*/ map(0xff800000, 0xffffffff).rom().region("maincpu", 0); } 

Al final del mismo archivo "drivers / midtunit.cpp", vemos cómo los contenidos de las EPROM se cargan en la RAM. En el caso de los recursos gráficos "gfxrom" (asociados con la dirección 0x02000000), podemos ver que abarcaron 8 mebibytes de espacio de direcciones en bloques de chips con alternancia cuádruple de direcciones. Tenga en cuenta que los nombres de los archivos corresponden a la ubicación de los chips (por ejemplo, UJ12 / UG12). El conjunto de estos archivos EPROM en el mundo de los emuladores se conoce mejor como "ROM".

 ROM_START( nbajamte ) ROM_REGION( 0x50000, "adpcm:cpu", 0 ) /* sound CPU*/ ROM_LOAD( "l1_nba_jam_tournament_u3_sound_rom.u3", 0x010000, 0x20000, NO_DUMP) ROM_RELOAD( 0x030000, 0x20000 ) ROM_REGION( 0x100000, "adpcm:oki", 0 ) /* ADPCM*/ ROM_LOAD( "l1_nba_jam_tournament_u12_sound_rom.u12", 0x000000, 0x80000, NO_DUMP) ROM_LOAD( "l1_nba_jam_tournament_u13_sound_rom.u13", 0x080000, 0x80000, NO_DUMP) ROM_REGION16_LE( 0x100000, "maincpu", 0 ) /* 34010 code*/ ROM_LOAD16_BYTE( "l4_nba_jam_tournament_game_rom_uj12.uj12", 0x00000, 0x80000, NO_DUMP) ROM_LOAD16_BYTE( "l4_nba_jam_tournament_game_rom_ug12.ug12", 0x00001, 0x80000, NO_DUMP) ROM_REGION( 0xc00000, "gfxrom", 0 ) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug14.ug14", 0x000000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj14.uj14", 0x000001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug19.ug19", 0x000002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj19.uj19", 0x000003, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug16.ug16", 0x200000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj16.uj16", 0x200001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug20.ug20", 0x200002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj20.uj20", 0x200003, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug17.ug17", 0x400000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj17.uj17", 0x400001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug22.ug22", 0x400002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj22.uj22", 0x400003, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug18.ug18", 0x600000, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj18.uj18", 0x600001, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_ug23.ug23", 0x600002, 0x80000, NO_DUMP) ROM_LOAD32_BYTE( "l1_nba_jam_tournament_game_rom_uj23.uj23", 0x600003, 0x80000, NO_DUMP) ROM_END 

Un hecho interesante: en el ejemplo de código anterior, el último parámetro de la función se reemplazó con "NO_DUMP" para poder cargar las EPROM modificadas. Estos campos suelen ser [12] un hash CRC / SHA1 del contenido de la EPROM. Así es como MAME determina qué juego pertenece a la ROM y le permite saber que una de las ROM del conjunto falta o está dañada.

Motor de video del corazón: DMA2


La clave para comprender el formato gráfico es la función que procesa la escritura / lectura de DMA en 256 registros DMA2, ubicados en direcciones de 0x01a80000 a 0x01a800ff. Los desarrolladores de MAME ya han hecho todo el trabajo duro de la ingeniería inversa. Incluso se tomaron el tiempo para documentar excelentemente el formato del comando.

  Registros DMA
  ------------------

   Registrarse |  Bit |  Solicitud
  ---------- + - FEDCBA9876543210 - + ------------
      0 |  xxxxxxxx -------- |  píxeles descartados al comienzo de cada fila
            El |  -------- xxxxxxxx |  píxeles descartados al final de cada fila
      1 |  x --------------- |  habilitar la grabación (o borrar si es cero)
            El |  -421 ------------ |  imágenes bpp (0 = 8)
            El |  ---- 84 ---------- |  tamaño de pase después de = (1 << x)
            El |  ------ 21 -------- |  tamaño de pase hasta = (1 << x)
            El |  -------- 8 ------- |  habilitar omitir antes / después
            El |  --------- 4 ------ |  habilitar el truncamiento
            El |  ---------- 2 ----- |  y reflejando
            El |  ----------- 1 ---- |  x reflejo
            El |  ------------ 8 --- |  transferir píxeles distintos de cero como colores
            El |  ------------- 4-- |  transmitiendo cero píxeles como colores
            El |  -------------- 2- |  transmisión de píxeles distintos de cero
            El |  --------------- 1 |  transmisión de cero píxeles
      2 |  xxxxxxxxxxxxxxxx |  dirección de origen palabra baja
      3 |  xxxxxxxxxxxxxxxx |  dirección de origen de palabra alta
      4 |  ------- xxxxxxxxx |  x destinatario
      5 |  ------- xxxxxxxxx |  y destinatario
      6 |  ------ xxxxxxxxxx |  columnas de imagen
      7 |  ------ xxxxxxxxxx |  lineas de imagen
      8 |  xxxxxxxxxxxxxxxx |  paleta
      9 |  xxxxxxxxxxxxxxxx |  color
     10 |  --- xxxxxxxxxxxxx |  escala x
     11 |  --- xxxxxxxxxxxxx |  escala y
     12 |  ------- xxxxxxxxx |  Recorte superior / izquierda
     13  ------- xxxxxxxxx |  Recorte inferior / derecha
     14  ---------------- |  la prueba
     15 |  xxxxxxxx -------- |  byte de detección cero
            El |  -------- 8 ------- |  página adicional
            El |  --------- 4 ------ |  tamaño del destinatario
            El |  ---------- 2 ----- |  selección del borde superior / inferior o izquierdo / derecho para el registro 12/13 

Incluso hay una función de depuración que le permite guardar los sprites originales en el proceso de transferirlos a DMA2 (la función fue escrita por un participante de mucho tiempo en el proyecto MAME, Ryan Holtz [13] ). Fue suficiente para mí simplemente jugar el juego para que todos los archivos con metadatos se guardaran en el disco.

Resultó que los sprites están formados por elementos simples de una paleta de 16 bits sin compresión. Sin embargo, no todos los sprites tienen el mismo número de colores. Algunos sprites usan solo 16 colores con índices de color de 4 bits, mientras que otros usan 256 colores y requieren índices de color de 8 bits.

Parche


Ahora sé la ubicación y el formato de los sprites, por lo que queda por realizar la cantidad mínima de ingeniería inversa. Escribí un pequeño programa en Golang para eliminar la alternancia de EPROM "código" y "gfx". Al eliminar las rayas, es fácil buscar ASCII o valores conocidos, porque trabajé exactamente con el aspecto de RAM durante la ejecución del programa.

Después de eso, puedes encontrar fácilmente las características del jugador. Resultó que todos estaban almacenados uno tras otro en un formato big-endian sin signo de 16 bits (lo cual es muy lógico, porque 34010 funciona con big-endian). Agregué un parche para modificar los atributos del jugador. No estoy realmente interesado en el baloncesto, ingresé SPEED = 9, 3 PTS = 9, DUNKS = 9, PASS = 9, POWER = 9, STEAL = 9, BLOCK = 9 y CLTCH = 9.

También escribí el código para parchar el juego con nuevos sprites con la única restricción: los nuevos sprites deben tener el mismo tamaño que los reemplazables. Para la foto de MJ, creé un PNG indexado de 256 colores (puede verlo aquí ).

Finalmente, agregué código para convertir el formato intermedio al formato intercalado para escribir en archivos EPROM individuales.

Comienza el juego



Después de parchear el contenido de la EPROM, la herramienta de diagnóstico NBAJam mostró que el contenido de algunos chips está marcado como "MALO". Lo esperaba porque solo parcheé el contenido de las EPROM, pero no me molesté en buscar el formato CRC e incluso su ubicación de almacenamiento.

Las EPROM GFX están marcadas en rojo (UG16 / UJ16, UG17 / UJ17, UG18 / UJ18, UG20 / UJ20, UG22 / UJ22 y UG23 / UJ23), porque contienen imágenes que cambié. Las dos EPROM en las que se almacenan las instrucciones (UG12 y UJ12) también son rojas, porque hay paletas.

Afortunadamente, aquí los CRC no se usan para proteger contra el contenido modificado y solo son necesarios para verificar la integridad de los chips. El juego ha comenzado. Y ganado!


Hasta La Vista, bebé!



Al terminar con dificultades técnicas, rápidamente perdí interés en la herramienta y dejé de desarrollarla. Ideas para aquellos que quieren jugar con el código:

  • Añadir a la Conferencia Este Toronto Raptors.
  • Agregue la capacidad de cambiar los nombres de los jugadores. Desafortunadamente, no consisten en ASCII, sino que son imágenes pregeneradas.

Libro sobre NBA Jam


Si eres fanático de la NBA Jam, entonces Reyan Ali escribió un libro completo sobre ella [14] . Puedes comprarlo aquí .

Código fuente


Si desea contribuir o simplemente ver cómo funciona todo, entonces la fuente completa se carga en github aquí .

Referencias


[1] Fuente: 'NJA Jam' de Reyan Ali

[2] Fuente: 'NJA Jam' de Reyan Ali

[3] Fuente: 'NJA Jam' de Reyan Ali

[4] Fuente: Mortal Kombat 1 Detrás de escena

[5] Fuente: 'NJA Jam' de Reyan Ali

[6] Fuente: 4: 3 versus píxeles cuadrados

[7] Comentario: Desafortunadamente, la era de tan excelente documentación ha pasado mucho tiempo

[8] Fuente: pantalla de inicio de Mame NBA Jam

[9] Fuente: conjunto de instrucciones TMS34010

[10] Fuente: Guía del usuario de T34010

[11] Fuente: NBA Jam - video de BoomShakaLaka

[12] Fuente: MAME T-Unit driver.cpp

[13] Fuente: Commit 'midtunit.cpp: se agregó un visor opcional DMA-blitter'

[14] Fuente: 'NBA JAM Book' por Reyan Ali

Source: https://habr.com/ru/post/473660/


All Articles