Arcade Reverse Engineering: Record Michael Jordan at NBA Jam







Last summer I was invited to a party in Sunnyvale. It turned out that the owners in the garage have an arcade machine NBA JAM Tournament Edition for four players. Despite the fact that the game has been over 25 years old (it was released in 1993), it is still very interesting to play it, especially for enthusiastic fans.



I was surprised by the Chicago Bulls list of players that didn't include Michael Jordan. According to sources [1] , MJ obtained its own license and was not part of the deal Midway made with the NBA.



After asking the owner of the machine, I learned that the hackers released a mod for SNES "NBA Jam 2K17", which allows new players and MJ to play, but no one was analyzing how the arcade version worked. Therefore, I definitely had to look inside.



Background



The story of the NBA Jam begins not with basketball, but with Jean-Claude Van Damme. Around the same time that Universal Soldier was released, Midway Games developed technology to manipulate large, digitized, photorealistic sprites that retain a resemblance to real actors. It was a huge technological breakthrough: animations with 60 frames per second, unprecedented sprites of 100x100 pixels in size, each of which had its own 256-color palette.



The company used this technology with great success in the popular shooter “Terminator 2: Judgment Day” [2] , but was unable to acquire a license for “Universal Soldier” (the financial conditions of JCVD ​​were unacceptable for Midway [3] ). When the negotiations ended in failure, Midway changed course and began developing a 1991 Capcom mega-hit fighting game called Street Fighter II: The World Warrior.



A team of four was assembled (Ed Boone wrote the code, John Tobias worked on art and script, John Vogel drew graphics, and Dan Forden was a sound engineer). After a year of hard work [4] Midway launched the Mortal Kombat in 1992.



The visual style was very different from the usual pixel art, and the game design was, to say the least, "controversial." The game with liters of blood on the screen and insanely cruel pushing- “fatality” instantly became a world hit and earned almost $ 1 billion over the year [5] .









SF2: 384 × 224 with 4,096 colors.









MK: 400 × 254 with 32,768 colors.



Interesting fact: like in VGA Mode 0x13 on PC, in these games the pixels were not square. Although the Mortal Kombat frame buffer is 400 × 254 in size, it is stretched to a 4: 3 CRT screen ratio, providing a resolution of 400 × 300 [6]



Midway T-Unit Equipment



The hardware developed by Midway for Mortal Kombat turned out to be very good. So good that he was given his own name T-Unit and reused in other games.





T-Unit consists of two boards. Most of them deal with game logic and graphics.









NBA JAM TE Edition processor board (approximately 40x40 cm, or 15 inches).









The other board is less complicated, but also capable of much. It is designed for audio, but it can play not only music using FM synthesis, but also digital sound.



The sound card is connected to a power source and a graphics card installed at the rear. Pay attention to the huge radiator located in the upper left corner.



Together, these two boards contain more than two hundred chips, resistors, and EPROM. Understanding all this only on the basis of serial numbers would be very time consuming. But, surprisingly, sometimes in devices from the 90s documentation is accidentally discovered. And in the case of the NBA Jam, she was just great.



Midway T-Unit Architecture



Looking for data, I came across an NBA Jam Kit. The level of detail of this document is amazing [7] . Among other things, I managed to find a detailed description of wiring connections, including EPROMs and chips.









The information from the document allowed us to draw a diagram of the boards and determine the function of each part. To assist in the search for components, the board has coordinates with a start in the lower right corner (UA0), increasing to the upper left corner (UJ26).









The heart of the main board is Texas Instrument TMS34010 (UB21) with a frequency of 50 MHz and with 1 mebibyte code in EPROMs and 512 kibibytes DRAM [8] . 34010 is a 32-bit chip with a 16-bit bus, which has such remarkable graphic instructions as PIXT and PIXBLT [9] . In the early 90s, this chip was used in several hardware acceleration cards [10] , and I thought that it handles a considerable amount of graphic effects. Surprisingly, he only deals with game logic and draws nothing.



In fact, the UE13 chip called “DMA2” turned out to be a graphic monster. According to the diagrams from the documentation, it has an impressive (at that time) 32-bit data bus and 32-bit address bus, which is why it became the largest chip on the board. This specialized integrated circuit (ASIC) is capable of many graphical operations, which I will discuss below.



All chips (System RAM, GFX EPROM, Palette SDRAM, Code, Video Banks) are mapped to one 32-bit address space and connected to the same bus. I could not find any information about the bus protocol, so if you know anything about it, write to e-mail.



Pay attention to a trick: one EPROM component (marked in blue) is used to create another storage system (and save money). These 512 kb EPROMs have 32-bit address pins and 8-bit data pins. For 34010, which requires a 16-bit data bus, two EPROMs (J12 and G12) are connected with a double alternation of addresses, creating a memory of 1 mebibyte. Similarly, graphic resources are connected with a fourfold alternation of addresses to form a 32-bit address with a 32-bit storage system containing 8 mebibytes.



Although in this article I will mainly consider the graphics pipeline, I can not resist the temptation, and therefore I will briefly talk about the audio system.









The sound card diagram shows the Motorola 6809 (U4 with a frequency of 2 MHz), which receives instructions from one EPROM (U3) to control music and sound effects.



The Yamaha 2151 FM synthesis chip (3.5 MHz) generates music directly from instructions received from 6809 (music uses a fairly small bandwidth).



OKI6295 (1 MHz) is responsible for playing digital audio in the ADPCM format (for example, the legendary “Boomshakalaka” [11] Tim Kitzrow).



Note that on the main board the same blue 512-kbyte EPROM 32a / 8d are used in a 16-bit system with double interleaving of addresses for storing digitized voices, but for 8-bit instructions there are no interleaving of data / addresses of the Motorola 6809.



Frame life



The entire NBA Jam screen is indexed in a 16-bit palette. Colors are stored in xRGB 1555 format in a 64-kbyte palette. The palette is divided into 128 blocks (256 * 16 bits) of 512 bytes. Sprites stored in EPROM are marked as “GFX”. Each sprite has its own palette up to 256x16-bit colors. Sprite often uses the whole block of the palette, but never more than one. A CRT signal is transmitted to the monitor using RAMDAC, which for each pixel reads the index from the Video DRAM banks and performs a color search in the palette.



The life of each frame of an NBA Jam video proceeds as follows:



  1. Game logic consists of a stream of 16-bit instructions transmitted from J12 / G12 to 34010.
  2. 34010 reads player input, calculates game status, and then draws a screen.
  3. To draw on the screen, 34010 first finds an unused block in the palette and writes the sprite palette there (sprite palettes are stored along with instructions 34010 in J12 / G12).
  4. 34010 performs a request to DMA2, which includes the address and size of the sprite, the 8-bit palette block used, truncation, scaling, the method of processing transparent pixels, and so on.
  5. DMA2 reads 8-bit sprite indices from the J14-G23 GFX ROM chip, combines this value with the index of an 8-bit palette block and writes a 16-bit index to video banks. DRAM2 can be considered a blitter that reads 8-bit values ​​from GFX EPROM and writes 16-bit values ​​to video banks
  6. Steps 3-5 are repeated until all requests for drawing sprites are completed.
  7. When it comes to screen refresh, RAMDAC converts the data in the video banks into a signal that a CRT monitor can understand. So that the bandwidth is enough to convert the 16-bit index to 16-bit RGB, the palette is stored in an extremely expensive and extremely fast SRAM.








An interesting fact: EPROM flash firmware is not such a simple process. Before writing to the chip, you must completely erase all its contents.



To do this, the chip must be irradiated with UV light. First you need to unstick the sticker from the top of the EPROM to open its scheme. Then EPROM is placed in a special eraser device, in which there is a UV lamp.



After 20 minutes, the EPROM will be filled with zeros and ready to record.



MAME Documentation



Having figured out the equipment, I realized what set of EPROM Michael Jordan could write to (the palette is stored in Code EPROMs, and the indices in GFX EPROMs). However, I still did not know the exact location or the format used.



Missing documentation found in MAME.



In case you don’t know how this awesome emulator works, I’ll briefly explain. MAME is built on the basis of the concept of "drivers", which are an imitation of the board. Each driver is made up of components that mimic (usually) each chip. In the case of Midway T-Unit, we are interested in the following files:



  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 


If you look at drivers / midtunit.cpp, we will see that each memory chip is part of a single 32-bit address space. It can be seen from the driver source code that the palette starts at 0x01800000, gfxrom starts at 0x02000000, and the DMA2 chip starts at 0x01a80000. To follow the data path, we need to track the C ++ functions that are executed when the object of the read or write operation is the memory address.



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





At the end of the same file “drivers / midtunit.cpp” we see how the contents of the EPROMs are loaded into RAM. In the case of the “gfxrom” graphic resources (associated with the address 0x02000000), we can see that they spanned 8 mebibytes of address space in chip blocks with four-fold alternation of addresses. Note that the file names correspond to the location of the chips (for example, UJ12 / UG12). The set of these EPROM files in the world of emulators is better known as “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
      
      





An interesting fact: in the above code example, the last parameter of the function was replaced with “NO_DUMP” so that modified EPROMs could be loaded. These fields are typically [12] a CRC / SHA1 hash of the contents of the EPROM. This is how MAME determines which game belongs to ROM and lets you know that one of the ROMs in the set is missing or damaged.



Heart video engine: DMA2



The key to understanding the graphics format is the function that processes DMA write / read in 256 DMA2 registers located at addresses from 0x01a80000 to 0x01a800ff. All the hard work of reverse engineering has already been done by the MAME developers. They even took the time to excellently document the command format.



  DMA Registers
  ------------------

   Register |  Bit |  Application
  ---------- + - FEDCBA9876543210 - + ------------
      0 |  xxxxxxxx -------- |  pixels discarded at the beginning of each row
            |  -------- xxxxxxxx |  pixels discarded at the end of each row
      1 |  x --------------- |  enable recording (or clear if zero)
            |  -421 ------------ |  bpp images (0 = 8)
            |  ---- 84 ---------- |  pass size after = (1 << x)
            |  ------ 21 -------- |  pass size up to = (1 << x)
            |  -------- 8 ------- |  enable skip before / after
            |  --------- 4 ------ |  enable truncation
            |  ---------- 2 ----- |  y mirroring
            |  ----------- 1 ---- |  x mirroring
            |  ------------ 8 --- |  transfer non-zero pixels as colors
            |  ------------- 4-- |  transmitting zero pixels as colors
            |  -------------- 2- |  non-zero pixel transmission
            |  --------------- 1 |  zero pixel transmission
      2 |  xxxxxxxxxxxxxxxx |  source address low word
      3 |  xxxxxxxxxxxxxxxx |  high word source address
      4 |  ------- xxxxxxxxx |  x recipient
      5 |  ------- xxxxxxxxx |  y recipient
      6 |  ------ xxxxxxxxxx |  image columns
      7 |  ------ xxxxxxxxxx |  image lines
      8 |  xxxxxxxxxxxxxxxx |  palette
      9 |  xxxxxxxxxxxxxxxx |  Colour
     10 |  --- xxxxxxxxxxxxx |  x scale
     11 |  --- xxxxxxxxxxxxx |  y scale
     12 |  ------- xxxxxxxxx |  trimming top / left
     13 |  ------- xxxxxxxxx |  trimming bottom / right
     14 |  ---------------- |  test
     15 |  xxxxxxxx -------- |  zero detection byte
            |  -------- 8 ------- |  additional page
            |  --------- 4 ------ |  recipient size
            |  ---------- 2 ----- |  selection of top / bottom or left / right edge for register 12/13 


There is even a debugging function that allows you to save the original sprites in the process of transferring them to DMA2 (the function was written by a longtime participant in the MAME project, Ryan Holtz [13] ). It was enough for me to simply play the game so that all files with metadata were saved to disk.



It turned out that sprites are made up of simple elements of a 16-bit palette without compression. However, not all sprites have the same number of colors. Some sprites use only 16 colors with 4-bit color indices, while others use 256 colors and require 8-bit color indices.



Patch



Now I know the location and format of the sprites, so it remains to perform the minimum amount of reverse engineering. I wrote a small program on Golang to eliminate the alternation of EPROMs “code” and “gfx”. By eliminating the striping, it is easy to search for ASCII or known values, because I worked exactly with what RAM looks like during program execution.



After that, you can easily find the characteristics of the player. It turned out that all of them were stored one after another in a 16-bit unsigned big-endian format (which is very logical, because 34010 works with big-endian). I added a patcher to modify player attributes. Not really keen on basketball, I entered SPEED = 9, 3 PTS = 9, DUNKS = 9, PASS = 9, POWER = 9, STEAL = 9, BLOCK = 9 and CLTCH = 9.



I also wrote the code for patching the game with new sprites with the only restriction - new sprites should have the same sizes as replaceable ones. For a photograph of MJ, I created a 256-color indexed PNG (you can see it here ).



Finally, I added code to convert the intermediate format to the interleaved format for writing to individual EPROM files.



Start the game









After patching the contents of the EPROM, the NBAJam diagnostic tool showed that the contents of some chips were marked as “BAD”. I expected this because I only patched the contents of the EPROMs, but did not bother looking for the CRC format and even their storage location.



GFX EPROMs are marked in red (UG16 / UJ16, UG17 / UJ17, UG18 / UJ18, UG20 / UJ20, UG22 / UJ22 and UG23 / UJ23), because they contain images that I changed. The two EPROMs in which the instructions (UG12 and UJ12) are stored are also red, because there are palettes.



Fortunately, here CRCs are not used to protect against modified content and are only needed to verify the integrity of the chips. The game has started. And earned!









Hasta La Vista, Baby!




Having finished with technical difficulties, I quickly lost interest in the tool and stopped developing it. Ideas for those who want to play around with the code:





Book about NBA Jam



If you are a fan of the NBA Jam, then Reyan Ali wrote a whole book about her [14] . You can buy it here .



Source



If you want to contribute or just see how everything works, then the full source code is available on github here .



References



[1] Source: 'NJA Jam' by Reyan Ali



[2] Source: 'NJA Jam' by Reyan Ali



[3] Source: 'NJA Jam' by Reyan Ali



[4] Source: Mortal Kombat 1 Behind The Scenes



[5] Source: 'NJA Jam' by Reyan Ali



[6] Source: 4: 3 versus Square Pixels



[7] Comment: Unfortunately, the era of such excellent documentation has long passed



[8] Source: Mame NBA Jam start-up screen



[9] Source: TMS34010 Instruction Set



[10] Source: T34010 User Guide



[11] Source: NBA Jam — BoomShakaLaka video



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



[13] Source: Commit 'midtunit.cpp: Added an optional DMA-blitter viewer'



[14] Source: 'NBA JAM Book' by Reyan Ali



All Articles