FABIEN SANGLARD'S WEBSITE

ABOUT  CONTACT  RSS  GIVE


Oct 28, 2019
A trip down NBA Jam graphics pipeline

I was invited to a party in Sunnyvale over the summer. The hosts happened to own a four-player arcade cabinet running NBA JAM Tournament Edition in their garage. Despite the game being 25 years old (it was released in 1993) it was a lot of fun to play, especially with four passionate players.

I was surprised by the Chicago Bulls roster from which Michael Jordan was absent. Reportedly[1], MJ managed his license himself and was not part of the deal Midway struck with the NBA.

Inquiring with the owner of the cabinet, I learned that hackers had released a SNES mod name "NBA Jam 2K17" allowing to play with new players and MJ. But nobody had taken the time to do figure out how the arcade version worked. That is all the reasons I needed to take a peek inside.

Genesis

The story of NBA Jam does not find its roots in basketball but rather, like all things, in Jean-Claude Van Damme. Around the same time "Universal Soldier" got released, "Midway Games" had developed a technology allowing to manipulate large digitized photo-realist sprites and preserve the likeness of real actors. We are talking about a huge technological leap here with 60 frames per second animations and unseen-before 100x100 pixels sprites, each with their own 256 colors palette.

They had used the technology with great success for the popular shooter game "Terminator 2: Judgment Day"[2] but were unable to secure a license for "Universal Soldier" (JCVD financial conditions were deemed unacceptable[3]). When negotiation collapsed, Midway "pivoted" to build a combat game in the spirit of Capcom's 1991 mega-hit named "Street Fighter II: The World Warrior".

An initial team of four (Ed Boon for programming, John Tobias for art and story, John Vogel for graphics, and Dan Forden for sound design) assembled and got to work. Within a year of hard work[4], Midway released Mortal Kombat in 1992.

The visual style was a departure from the usual pixel-art and the game design was, to put it mildly, "controversial". Featuring a lot of blood on screen and insanely gore "fatalities", the game became an instant worldwide hit gathering close to 1 billion dollar within one year[5].

SF2: 384×224 with 4,096 colors. MK: 400×254 with 32,768 colors.


Trivia: Like the VGA Mode 0x13 on PC, these games have non square-pixels. Even though Mortal Kombat framebuffer is 400 × 254, it is stretched on a 4:3 CRT to 400 × 300[6].

Midway T-Unit hardware

The hardware Midway developed for Mortal Kombat turned out to be really good. So good that it was given a name, T-Unit, and reused for other games.

A T-Unit is made of two boards. The largest one is where game logic and graphics happen.

NBA JAM TE Edition CPU board (about 40cm by 40cm or 15 inches).

The other board is less complex in comparison but manage to perform a lot anyway. Dedicated to audio, it is not only capable to play music via FM synthesis but also digitized sound playback.

The sound board is connected to the power supply and to the graphic board, mounted beside it. Notice the huge passive heat sink on its upper left corner.

Together the two boards account for more than two hundreds chips, resistances, and EPROMs. To make sense of everything using only serial numbers would have been a daunting task. But the amazing thing about stuff from the 90s is that it is occasionally documented. And in the case of NBA Jam, it is beautifully documented.

Midway T-Unit architecture

In my quest for data, I stumbled upon the NBA Jam Kit. The level of details in this document is astonishing[7]. Among other things, we can find here an extended description of the boards wiring including EPROM and chip connections.


The informations from the kit allowed to draw a map of the boards and identify the function of each parts. To help reference components, a board has coordinates with origin in the lower-right (UA0) increasing to the upper-left (UJ26).


At the heart of the main board, we find a Texas Instrument TMS34010 (UB21) running at 50Mhz with 1MiB of code in EPROMs and 512 KiB DRAM[8]. The 34010 is a 32-bit chip with a 16-bit bus which features cool graphic instructions such as PIXT and PIXBLT[9]. This chip was used in several hardware-accelerating[10] card in the early 90s and I expected it to do a lot of GFX work. Surprisingly, it only takes care of the game logic and does not draw anything.

The graphic powerhouse is in fact the U13 chip called "DMA2". According to the kit diagrams, it has an impressive (at the time) 32-bit data bus and a 32-bit address bus, making it the biggest chip of the board. The super-blitter ASIC is capable of many graphic operations prowess which are detailed later.

All chips (System RAM, GFX EPROMs, Palette SDRAM, Code, Video Banks) are mapped into the same 32-bit memory address space and are connected to the same bus. I was unable to dig anything about the bus protocol so if you have any knowledge of the matter, please email me.

Notice the cool trick of using the same EPROM component (in blue) to build different storage system (and save money). These 512 KiB EPROMS have 32-bit address pins and 8-bit data pins. For the 34010, which needs a 16-bit data bus, two EPROMS (J12 and G12) are two-way interleaved into 1 MiB. In the same fashion, the graphic assets are four-way interleaved to form a 32-bit address, 32-bit data storage system storing 8MiB.

Event though this article is mostly about the graphic pipeline, I cannot resist describing briefly the audio system.

On the right, the map of the sound board details how the Motorola 6809 (U4 running at 2Mhz) is fed instructions from a single EPROM (U3) to orchestrate the music and sound effects.

The Yamaha 2151 FM synthesis chip (3.5Mhz) generates musics directly from instructions received from the 6809 (musics are using fairly low bandwidth).

The OKI6295 (1Mhz) is in charge of digitized audio ADPCM playback (such as Tim Kitzrow legendary "Boomshakalaka"[11]).

Notice how, like in the main board, the same blue 512KiB 32a/8d EPROMs are used in a 16-bit two-way interleave system for the digitized voices storage and there is no interleave for the 8-bit data/address Motorola 6809 instructions.

Life of a frame

The whole NBA Jam screen is 16-bit palette indexed. Colors are stored as xRGB 1555 in a 64 KiB palette. The palette is divided in 128 blocks of (256 * 16-bit) 512 bytes. Sprites are stored in the EPROM set labeled "GFX". Each sprite has its own palette with up to 256x16-bit colors. A sprite often uses one palette block but never more than one. CRT signal is sent to the monitor by a RAMDAC which, for each pixel, reads an index from the Video DRAM banks and lookup the color in the palette.

The life of a NBA Jam video frame is as follow:

  1. The game logic made of 16-bit instructions flow from J12/G12 to the 34010.
  2. The 34010 reads player inputs, computes the game state and then draws the screen.
  3. To draw anything to the screen, the 34010 first find an unused block in the palette and writes the sprite palette there (sprite palettes are stored along with the 34010 instructions in J12/G12).
  4. The 34010 issues a request to the DMA2 which includes the sprite's address, dimensions, 8-bit palette block used, clipping, scaling, what to do with transparent pixels and so on.
  5. The DMA2 reads 8-bit sprite indexes from J14-G23 GFX ROMs, combine that value with the 8-bit palette block index and write the 16-bit index into the Video banks. The DMA2 can be seen as a blitter reading 8-bit values from GFX EPROM and writing 16-bit values in the Video banks
  6. Steps 3-5 repeat until all sprites drawing request have been completed.
  7. When the screen needs to be refreshed, the RAMDAC convert what is in the Video banks into a signal the CRT can understand. To keep up with the bandwidth requirements of converting 16-bit index to 16-bit RGB, the palette is stored in extremely expensive and extremely fast SRAM.

Trivia: Flashing an EPROM is not a straight-forward process. Before writing to the chip, its content needs to be completely erased.

To do that, the chip must be bombarded with UV light. Start by peeling off the sticker on top of the EPROM in order to expose its circuitry. Then place the EPROM face up in a special eraser which contains a UV lamp.

After a 20 minutes nap the EPROM is full of 1s and ready to be written (and you can only write 0s).

MAME documentation

With an understanding of the hardware, I knew in which set of EPROMs Michael would have to find its way (palette in Code EPROMs and indices in GFX EPROMs). However i still had no idea about the exact location and the graphic format to use.

The missing documentation was located in MAME. If you are unfamiliar with how the amazing emulator works, here is a quick introduction. MAME is built around the concept of "drivers" which replicate a board. Each driver is made of components replicating (usually) each chips. In the case of the Midway T-Unit, all files of interest are here.

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 we look at drivers/midtunit.cpp, we can see that every memory chip is part of a single 32-bit address space. The driver source code reveal how the palette starts at 0x01800000, the gfxrom at 0x02000000, and the DMA2 chip at 0x01a80000. To follow the data path, one has to follow the C++ functions triggered when a memory address is subject to a read or write operation.

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

In the same "drivers/midtunit.cpp" file, located at the bottom, we can see how the EPROMs content is loaded in RAM. In the case of the graphic assets "gfxrom" (mapped at 0x02000000), we can see they span over eight MiBs of address space in blocks of four-interleaved chips. Notice how the filenames match the chip location (e.g: UJ12/UG12). A set of these EPROMs files is more commonly known as "ROMs" in the world of emulators.

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

Trivia: In the code sample above, the last function parameter was changed to "NO_DUMP" in order to allowed modded EPROM to load. These fields are usually[12] a CRC/SHA1 hash of the EPROM content. This is how MAME identify which ROM belongs to what game and is able to tell you if one ROM in the set is missing/corrupted.

The heart of the video engine: DMA2

The key to understand the graphic format is in the function handing DMA write/read to the DMA2 256 registers spanning from 0x01a80000 to 0x01a800ff. All the hard work of reverse-engineering had already been done by MAME developers. They even took the time to beautifully document the command format.

 DMA registers
 ------------------

  Register | Bit              | Use
 ----------+-FEDCBA9876543210-+------------
     0     | xxxxxxxx-------- | pixels to drop at the start of each row
           | --------xxxxxxxx | pixels to drop at the end of each row
     1     | x--------------- | trigger write (or clear if zero)
           | -421------------ | image bpp (0=8)
           | ----84---------- | post skip size = (1<<x)
           | ------21-------- | pre skip size = (1<<x)
           | --------8------- | pre/post skip enable
           | ---------4------ | clipping enable
           | ----------2----- | flip y
           | -----------1---- | flip x
           | ------------8--- | blit nonzero pixels as color
           | -------------4-- | blit zero pixels as color
           | --------------2- | blit nonzero pixels
           | ---------------1 | blit zero pixels
     2     | xxxxxxxxxxxxxxxx | source address low word
     3     | xxxxxxxxxxxxxxxx | source address high word
     4     | -------xxxxxxxxx | detination x
     5     | -------xxxxxxxxx | destination y
     6     | ------xxxxxxxxxx | image columns
     7     | ------xxxxxxxxxx | image rows
     8     | xxxxxxxxxxxxxxxx | palette
     9     | xxxxxxxxxxxxxxxx | color
    10     | ---xxxxxxxxxxxxx | scale x
    11     | ---xxxxxxxxxxxxx | scale y
    12     | -------xxxxxxxxx | top/left clip
    13     | -------xxxxxxxxx | bottom/right clip
    14     | ---------------- | test
    15     | xxxxxxxx-------- | zero detect byte
           | --------8------- | extra page
           | ---------4------ | destination size
           | ----------2----- | select top/bottom or left/right for reg 12/13
 

There is even a debug feature allowing to save source sprites as the DMA2 blits them (written by long-time MAME contributor, Ryan Holtz[13]). All I had to do was to play the game to get all sprites saved on disk along with metadata.

It turned out sprites are made of plain 16-bit palette entries with no compression. All sprites don't have the same number of colors however. Some sprite use only 16 colors with 4-bit color indexes while some other use 256 colors requesting 8-bit color indexes.

Patching

With knowledge of sprite location and format, I had only a minimal amount of reverse-engineering to perform. I wrote a small Golang program to de-interleave the "code" and "gfx" EPROMs. With de-interleaved content, it was easy to search for ASCII or known values since I worked with exactly what the RAM looked like at runtime.

From there, finding player stats was easy. It turns out their were all stored one after an other as 16-bit unsigned big-endian (which made a lot of sense since the 34010 is BE). I added a patcher to modify player attributes. Not knowing basketball much I went with SPEED=9, 3 PTS=9, DUNKS=9, PASS=9, POWER=9, STEAL=9, BLOCK=9, and CLTCH=9.

I also wrote something to patches new sprites with the only constraint that new sprites have to be the same dimensions as what it overwrites. For MJ photo, I crafted a 256 colors indexed PNG (available here).

Finally I added something to interleave the intermediate format into individual burnable EPROMs files.

Running it

After patching the content of the EPROMs, NBAJam diagnostic tool showed that several chips content were "BAD". This was expected since I patched the content of the EPROMs but did not bother to find the CRC format or even where it was stored.

The GFX EPROMS are marked red (UG16/UJ16, UG17/UJ17, UG18/UJ18, UG20/UJ20, UG22/UJ22, and UG23/UJ23) because this is where images I touched are stored. The two EPROMs storing the instructions (UG12 and UJ12) are also red because this is where the palettes are stored.

Luckily CRCs are not used here to prevent modded content but only to check chips integrity. The game proceeded to startup. And it worked :) !

NeXT

With the technical challenge gone, I quickly lost interest in the tool and stopped developing it. Ideas for someone willing to toy with the code:

Further reading

If you are a fan of NBA Jam, Reyan Ali wrote a whole book[15] about it. You can get it here.

If you want to explore the architecture of the T-Unit further, you may find it interesting to go through the previous generation, the Z-UNIT, Theory and Maintenance Manual[16] which was used for NARC.

Source Code

If you want to contribute of merely look how it is done, the full source code is available on github here.

References

^ [ 1]'NJA Jam' by Reyan Ali
^ [ 2]'NJA Jam' by Reyan Ali
^ [ 3]'NJA Jam' by Reyan Ali
^ [ 4]Mortal Kombat 1 Behind The Scenes
^ [ 5]'NJA Jam' by Reyan Ali
^ [ 6]4:3 versus Square Pixels
^ [ 7]Sadly the days of gorgeous documentation like this instance are long gone.
^ [ 8]Mame NBA Jam start-up screen
^ [ 9]TMS34010 Instruction Set
^ [10]T34010 User Guide
^ [11]NBA Jam—BoomShakaLaka video
^ [12]MAME T-Unit driver.cpp
^ [13]Commit 'midtunit.cpp: Added an optional DMA-blitter viewer'
^ [14]Williams OKI Editor
^ [15]'NBA JAM Book' by Reyan Ali
^ [16]Z-UNIT Theory and Maintenance Manual (thanks Louis Koziarz)


*