Arcade Reverse Engineering: Record Michael Jordan no NBA Jam


No verão passado, fui convidado para uma festa em Sunnyvale. Acontece que os proprietários da garagem têm uma máquina de arcade NBA JAM Tournament Edition para quatro jogadores. Apesar do jogo já ter mais de 25 anos (lançado em 1993), ainda é muito interessante jogá-lo, principalmente para fãs entusiasmados.

Fiquei surpreso com a lista de jogadores do Chicago Bulls que não incluía Michael Jordan. Segundo fontes [1] , o MJ recebeu sua própria licença e não fazia parte do acordo que a Midway fez com a NBA.

Depois de perguntar ao proprietário da máquina, aprendi que os hackers lançaram um mod para o SNES "NBA Jam 2K17", que permite que novos jogadores e MJ joguem, mas ninguém estava analisando como a versão arcade funcionava. Portanto, eu definitivamente tive que olhar para dentro.

Antecedentes


A história do NBA Jam começa não com basquete, mas com Jean-Claude Van Damme. Na mesma época em que o Universal Soldier foi lançado, a Midway Games desenvolveu tecnologia para manipular sprites grandes, digitalizados e fotorrealistas, que mantêm uma semelhança com atores reais. Foi um grande avanço tecnológico: animações com 60 quadros por segundo, sprites inéditos de tamanho de 100x100 pixels, cada um com sua própria paleta de 256 cores.

A empresa usou essa tecnologia com grande sucesso no popular jogo de tiro "Terminator 2: Judgement Day" [2] , mas não conseguiu adquirir uma licença para "Universal Soldier" (as condições financeiras da JCVD ​​eram inaceitáveis ​​para Midway [3] ). Quando as negociações terminaram em fracasso, a Midway mudou de rumo e começou a desenvolver um jogo de luta de sucesso da Capcom de 1991 chamado Street Fighter II: The World Warrior.

Uma equipe de quatro foi montada (Ed Boone escreveu o código, John Tobias estava envolvido em arte e scripts, John Vogel desenhou gráficos e Dan Forden era um engenheiro de som). Após um ano de muito trabalho [4], a Midway lançou o Mortal Kombat em 1992.

O estilo visual era muito diferente do pixel art usual, e o design do jogo era, para dizer o mínimo, "controverso". O jogo com litros de sangue na tela e empurrões insanamente cruéis - “fatalidade” instantaneamente se tornou um sucesso mundial e ganhou quase 1 bilhão de dólares em um ano [5] .


SF2: 384 × 224 com 4.096 cores.


MK: 400 × 254 com 32.768 cores.

Fato interessante: como no modo VGA 0x13 no PC, nesses jogos os pixels não eram quadrados. Embora o buffer de quadro do Mortal Kombat tenha um tamanho de 400 × 254, ele é esticado para uma proporção de 4: 3 de uma tela CRT, fornecendo uma resolução de 400 × 300 [6]

Midway T-Unit Equipment


O hardware desenvolvido pela Midway para Mortal Kombat acabou por ser muito bom. Tão bom que ele recebeu seu próprio nome T-Unit e foi reutilizado em outros jogos.

  • Mortal Kombat.
  • Mortal Kombat II.
  • NBA Jam.
  • Edição da NBA Jam Tournament.
  • Juiz Dredd (não foi libertado).

A unidade T consiste em duas placas. A maioria deles lida com lógica e gráficos de jogos.


Placa processadora NBA JAM TE Edition (aproximadamente 40x40 cm ou 15 polegadas).


A outra diretoria é menos complicada, mas também capaz de muito. Ele é projetado para áudio, mas pode tocar não apenas música usando a síntese FM, mas também som digital.

A placa de som está conectada a uma fonte de energia e a uma placa gráfica instalada na parte traseira. Preste atenção ao enorme radiador localizado no canto superior esquerdo.

Juntas, essas duas placas contêm mais de duzentos chips, resistores e EPROM. Entender tudo isso apenas com base em números de série seria muito demorado. Mas, surpreendentemente, às vezes em dispositivos da documentação dos anos 90 é descoberto acidentalmente. E no caso da NBA Jam, ela era ótima.

Arquitetura de unidade T intermediária


Procurando dados, me deparei com um Kit de Jam da NBA. O nível de detalhe deste documento é incrível [7] . Entre outras coisas, consegui encontrar uma descrição detalhada das conexões de fiação, incluindo EPROMs e chips.


As informações do documento nos permitiram desenhar um diagrama das placas e determinar a função de cada parte. Para auxiliar na busca de componentes, a placa possui coordenadas com um começo no canto inferior direito (UA0), aumentando para o canto superior esquerdo (UJ26).


O coração da placa principal é o Texas Instrument TMS34010 (UB21) com uma frequência de 50 MHz e com 1 código de mebibyte em EPROMs e DRAM de 512 kibibytes [8] . 34010 é um chip de 32 bits com um barramento de 16 bits, que possui instruções gráficas notáveis ​​como PIXT e PIXBLT [9] . No início dos anos 90, esse chip foi usado em várias placas de aceleração de hardware [10] , e eu pensei que ele lida com uma quantidade considerável de efeitos gráficos. Surpreendentemente, ele lida apenas com a lógica do jogo e não desenha nada.

De fato, o chip UE13 chamado “DMA2” acabou sendo um monstro gráfico. De acordo com os diagramas da documentação, ele possui um impressionante (naquele tempo) barramento de dados de 32 bits e um barramento de endereços de 32 bits, razão pela qual se tornou o maior chip da placa. Este circuito integrado especializado (ASIC) é capaz de muitas operações gráficas, as quais discutirei abaixo.

Todos os chips (RAM do sistema, GFX EPROM, SDRAM da paleta, código, bancos de vídeo) são mapeados para um espaço de endereço de 32 bits e conectados ao mesmo barramento. Não encontrei nenhuma informação sobre o protocolo de barramento; portanto, se você souber algo sobre isso, escreva para o e-mail.

Preste atenção a um truque: um componente EPROM (marcado em azul) é usado para criar outro sistema de armazenamento (e economizar dinheiro). Essas EPROMs de 512 kb têm pinos de endereço de 32 bits e pinos de dados de 8 bits. Para o 34010, que requer um barramento de dados de 16 bits, duas EPROMs (J12 e G12) são conectadas com uma dupla alternância de endereços, criando memória em 1 mebibyte. Da mesma forma, os recursos gráficos são conectados a uma alternância de endereços quadruplicada para formar um endereço de 32 bits com um sistema de armazenamento de 32 bits contendo 8 mebibytes.

Embora neste artigo eu considere principalmente o pipeline de gráficos, não resisto à tentação e, portanto, falarei brevemente sobre o sistema de áudio.


O diagrama da placa de som mostra o Motorola 6809 (U4 com uma frequência de 2 MHz), que recebe instruções de uma EPROM (U3) para controlar a música e os efeitos sonoros.

O chip de síntese FM da Yamaha 2151 (3,5 MHz) gera música diretamente das instruções recebidas do 6809 (a música usa uma largura de banda bastante pequena).

O OKI6295 (1 MHz) é responsável por reproduzir áudio digital no formato ADPCM (por exemplo, o lendário "Boomshakalaka" [11] Tim Kittsrow).

Observe que na placa principal, o mesmo EPROM azul de 512 kbytes de 32a / 8d é usado em um sistema de 16 bits com intercalação dupla de endereços para armazenar vozes digitalizadas, mas para instruções de 8 bits, os dados / endereços do Motorola 6809 não são intercalados.

Vida do quadro


A tela inteira do NBA Jam é indexada em uma paleta de 16 bits. As cores são armazenadas no formato xRGB 1555 em uma paleta de 64 kbytes. A paleta é dividida em 128 blocos (256 * 16 bits) de 512 bytes. Sprites armazenados na EPROM estão marcados como "GFX". Cada sprite tem sua própria paleta de cores de até 256x16 bits. Um sprite geralmente usa um bloco de paleta inteiro, mas nunca mais que um. Um sinal CRT é transmitido ao monitor usando RAMDAC, que para cada pixel lê o índice dos bancos de DRAM de vídeo e realiza uma pesquisa de cores na paleta.

A vida de cada quadro de um vídeo da NBA Jam é a seguinte:

  1. A lógica do jogo consiste em um fluxo de instruções de 16 bits transmitidas de J12 / G12 a 34010.
  2. 34010 lê a entrada do jogador, calcula o status do jogo e desenha uma tela.
  3. Para desenhar na tela, o 34010 primeiro encontra um bloco não utilizado na paleta e grava a paleta sprite (as paletas sprite são armazenadas juntamente com as instruções 34010 em J12 / G12).
  4. 34010 faz uma solicitação ao DMA2, que inclui o endereço e o tamanho do sprite, o bloco de paleta de 8 bits usado, truncamento, dimensionamento, o método de processamento de pixels transparentes e assim por diante.
  5. O DMA2 lê índices de sprite de 8 bits do chip ROM J14-G23 GFX, combina esse valor com o índice de um bloco de paleta de 8 bits e grava um índice de 16 bits nos bancos de vídeo. A DRAM2 pode ser considerada um blitter que lê valores de 8 bits da GFX EPROM e grava valores de 16 bits em bancos de vídeo
  6. As etapas 3 a 5 são repetidas até que todas as solicitações de desenho de sprites sejam concluídas.
  7. Quando se trata de atualização de tela, o RAMDAC converte os dados nos bancos de vídeo em um sinal que um monitor CRT pode entender. Para que a largura de banda seja suficiente para converter o índice de 16 bits em RGB de 16 bits, a paleta é armazenada em uma SRAM extremamente cara e extremamente rápida.


Um fato interessante: o firmware flash da EPROM não é um processo tão simples. Antes de escrever no chip, você deve apagar completamente todo o seu conteúdo.

Para fazer isso, o chip deve ser irradiado com luz UV. Primeiro, você precisa soltar o adesivo da parte superior da EPROM para abrir seu diagrama. Em seguida, a EPROM é colocada em um dispositivo apagador especial no qual há uma lâmpada UV.

Após 20 minutos, a EPROM será preenchida com zeros e pronta para gravação.

Documentação MAME


Tendo descoberto o equipamento, percebi que conjunto de EPROM você poderia escrever para Michael Jordan (a paleta é armazenada nas EPROMs de código e os índices nas EPROMs GFX). No entanto, eu ainda não sabia a localização exata ou o formato usado.

Documentação ausente encontrada em MAME.

Caso você não saiba como esse incrível emulador funciona, explicarei brevemente. O MAME é baseado no conceito de "drivers", que são uma imitação do conselho. Cada driver é composto de componentes que imitam (geralmente) cada chip. No caso da Midway T-Unit, estamos interessados ​​nos seguintes arquivos:

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

Se você olhar para drivers / midtunit.cpp, veremos que cada chip de memória faz parte de um único espaço de endereço de 32 bits. Pode ser visto no código-fonte do driver que a paleta começa em 0x01800000, gfxrom começa em 0x02000000 e o chip DMA2 começa em 0x01a80000. Para seguir o caminho dos dados, precisamos seguir as funções C ++ executadas quando o objeto da operação de leitura ou gravação é o endereço da memória.

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); } 

No final do mesmo arquivo “drivers / midtunit.cpp”, vemos como o conteúdo das EPROMs é carregado na RAM. No caso dos recursos gráficos “gfxrom” (associados ao endereço 0x02000000), podemos ver que eles ocupavam 8 mebibytes de espaço de endereço em blocos de chips com alternância de endereços quatro vezes maior. Observe que os nomes dos arquivos correspondem ao local dos chips (por exemplo, UJ12 / UG12). O conjunto desses arquivos EPROM no mundo dos emuladores é mais conhecido 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 

Um fato interessante: no exemplo de código acima, o último parâmetro da função foi substituído por "NO_DUMP" para que as EPROMs modificadas pudessem ser carregadas. Esses campos são geralmente [12] um hash CRC / SHA1 do conteúdo da EPROM. É assim que o MAME determina qual jogo pertence à ROM e permite que você saiba que uma das ROMs do conjunto está ausente ou danificada.

Mecanismo de vídeo do coração: DMA2


A chave para entender o formato gráfico é a função que processa a gravação / leitura de DMA em 256 registros DMA2 localizados em endereços de 0x01a80000 a 0x01a800ff. Todo o trabalho árduo da engenharia reversa já foi realizado pelos desenvolvedores do MAME. Eles ainda tiveram tempo para documentar excelentemente o formato do comando.

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

   Registar |  Bit  Aplicação
  ---------- + - FEDCBA9876543210 - + ------------
      0  xxxxxxxx -------- |  pixels descartados no início de cada linha
            |  -------- xxxxxxxx |  pixels descartados no final de cada linha
      1 |  x --------------- |  ativar a gravação (ou limpar se zero)
            |  -421 ------------ |  imagens bpp (0 = 8)
            |  ---- 84 ---------- |  tamanho do passe após = (1 << x)
            |  ------ 21 -------- |  tamanho do passe até = (1 << x)
            |  -------- 8 ------- |  ativar pular antes / depois
            |  --------- 4 ------ |  ativar truncamento
            |  ---------- 2 ----- |  y espelhamento
            |  ----------- 1 ---- |  x espelhamento
            |  ------------ 8 --- |  transferir pixels diferentes de zero como cores
            |  ------------- 4-- |  transmitindo zero pixels como cores
            |  -------------- 2- |  transmissão de pixels diferente de zero
            |  --------------- 1 |  transmissão de pixel zero
      2  xxxxxxxxxxxxxxxx |  palavra baixa do endereço de origem
      3  xxxxxxxxxxxxxxxx |  endereço de origem de palavras altas
      4  ------- xxxxxxxxx |  x destinatário
      5  ------- xxxxxxxxx |  y destinatário
      6  ------ xxxxxxxxxx |  colunas de imagem
      7  ------ xxxxxxxxxx |  linhas de imagem
      8  xxxxxxxxxxxxxxxx |  paleta
      9  xxxxxxxxxxxxxxxx |  cor
     10  --- xxxxxxxxxxxxx |  escala x
     11  --- xxxxxxxxxxxxx |  escala y
     12  ------- xxxxxxxxx |  aparar superior / esquerdo
     13  ------- xxxxxxxxx |  aparar inferior / direita
     14  ---------------- |  o teste
     15  xxxxxxxx -------- |  byte de detecção zero
            |  -------- 8 ------- |  página adicional
            |  --------- 4 ------ |  tamanho do destinatário
            |  ---------- 2 ----- |  seleção da borda superior / inferior ou esquerda / direita para o registro 12/13 

Existe até uma função de depuração que permite salvar os sprites originais no processo de transferência para o DMA2 (a função foi escrita por um participante de longa data do projeto MAME, Ryan Holtz [13] ). Foi o suficiente para eu simplesmente jogar o jogo, para que todos os arquivos com metadados fossem salvos no disco.

Acontece que os sprites são compostos de elementos simples de uma paleta de 16 bits sem compactação. No entanto, nem todos os sprites têm o mesmo número de cores. Alguns sprites usam apenas 16 cores com índices de cores de 4 bits, enquanto outros usam 256 cores e requerem índices de cores de 8 bits.

Patch


Agora eu sei a localização e o formato dos sprites, então resta executar a quantidade mínima de engenharia reversa. Eu escrevi um pequeno programa no Golang para eliminar a alternância de EPROMs "code" e "gfx". Ao eliminar a distribuição, é fácil procurar valores ASCII ou conhecidos, porque trabalhei exatamente com a aparência da RAM durante a execução do programa.

Depois disso, você pode encontrar facilmente as características do jogador. Aconteceu que todos eles foram armazenados um após o outro no formato big endian não assinado de 16 bits (o que é muito lógico, porque 34010 funciona com big endian). Eu adicionei um patcher para modificar os atributos do jogador. Não gosto muito de basquete, entrei em SPEED = 9, 3 PTS = 9, DUNKS = 9, PASS = 9, POWER = 9, STEAL = 9, BLOCK = 9 e CLTCH = 9.

Também escrevi o código para consertar o jogo com novos sprites com a única restrição - novos sprites devem ter o mesmo tamanho dos substituíveis. Para a foto do MJ, criei um PNG indexado de 256 cores (você pode vê-lo aqui ).

Por fim, adicionei código para converter o formato intermediário no formato intercalado para gravar em arquivos EPROM individuais.

Comece o jogo



Depois de corrigir o conteúdo da EPROM, a ferramenta de diagnóstico NBAJam mostrou que o conteúdo de alguns chips está marcado como "RUIM". Eu esperava isso porque apenas corrigi o conteúdo das EPROMs, mas não me preocupei em procurar o formato CRC e até mesmo o local de armazenamento.

As EPROMs GFX estão marcadas em vermelho (UG16 / UJ16, UG17 / UJ17, UG18 / UJ18, UG20 / UJ20, UG22 / UJ22 e UG23 / UJ23), porque contêm imagens que eu alterei. As duas EPROMs nas quais as instruções (UG12 e UJ12) estão armazenadas também são vermelhas, porque existem paletas.

Felizmente, aqui os CRCs não são usados ​​para proteger contra conteúdo modificado e são necessários apenas para verificar a integridade dos chips. O jogo começou. E ganhou!


Hasta La Vista, bebê!



Tendo terminado com dificuldades técnicas, perdi rapidamente o interesse pela ferramenta e parei de desenvolvê-la. Ideias para quem quer brincar com o código:

  • Acrescente ao Eastern Conference Toronto Raptors.
  • Adicione a capacidade de alterar os nomes dos jogadores. Infelizmente, eles não consistem em ASCII, mas são imagens pré-geradas.

Livro sobre NBA Jam


Se você é fã do NBA Jam, então Reyan Ali escreveu um livro inteiro sobre ela [14] . Você pode comprá-lo aqui .

Código fonte


Se você deseja contribuir ou apenas ver como tudo funciona, a fonte completa é carregada no github aqui .

Referências


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

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

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

[4] Fonte: Mortal Kombat 1 nos bastidores

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

[6] Fonte: 4: 3 versus pixels quadrados

[7] Comentário: Infelizmente, a era dessa documentação excelente já passou há muito tempo.

[8] Fonte: Tela de inicialização do Mame NBA Jam

[9] Fonte: Conjunto de instruções TMS34010

[10] Fonte: Guia do usuário do T34010

[11] Fonte: NBA Jam - vídeo do BoomShakaLaka.

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

[13] Fonte: Commit 'midtunit.cpp: adicionado um visualizador DMA-blitter opcional'

[14] Fonte: 'NBA JAM Book' de Reyan Ali

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


All Articles