Midway Zeus 2
The "Zeus 2" was the successor to the original Midway Zeus 3D chip which powered games such as Mortal Kombat 4 and Invasion. This page describes the current state of understanding of the chip, based on reverse engineering the games.
Games
There are two known games running on the Zeus 2 hardware:
- Cruis'n Exotica
- The Grid
Main CPU
The main CPU driving the Zeus 2 is, like the original Zeus, a TMS32C032 DSP. It is assumed to run at 60 MHz like the original, although this has yet to be confirmed.
Zeus 2 3D Graphics
As with the original Zeus, the Zeus 2 chip is memory mapped into the main CPU's address space at addresses from $880000-$88007F. Keep in mind that the TMS32C032 only accesses 32-bit memory, so each of the 128 addresses is 32 bits wide (i.e., address $880000 references one 32-bit word, and address $880001 references a completely independent 32-bit word).
As with most external chips, the memory map for the Zeus consists of a number of registers, in this case 128 registers. Unlike the original Zeus, the Zeus 2 appears to only operate in full 32-bit mode.
Wave RAM
The Zeus 2 chip provides access to two banks of local RAM, called "Wave RAM" in the diagnostic tests. Wave RAM is organized into two separate banks, one for 3D rendering data, and one for the framebuffer. Most references to Wave RAM are done in terms of what are suspected to be row/column addresses.
Bank 0 of Wave RAM is used to store model data, texture data, palettes, and other rendering information. It is organized as 2048 rows by 1024 columns. Each "cell" of bank 0 contains 8 bytes, giving 2k × 1k × 8 = 16MB of RAM. Data is stored sequentially as little-endian 32-bit words (two words are latched and written together into a single cell).
Bank 1 of Wave RAM holds the frame buffer (2 pages). It is organized as 1024 rows by 512 columns. Each "cell" of bank 1 contains 12 bytes, giving 1k × 0.5k × 12 = 6MB of RAM. Frame buffer data is stored as 32-bit RGB data, with a 16-bit depth component. The data is packed such that two 32bpp pixels live in their own words, while the two corresponding 16-bit depth values are packed into a 3rd 32-bit word and stored within the same cell.
Wave RAM is generally addressed in row/column format, in the following format:
Bit | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 09 | 08 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
x | x | x | x | x | ROW | x | x | x | x | x | x | COLUMN |
Register Map
Address | R/W | Purpose |
---|---|---|
$08 | W | Data FIFO (see FIFO commands, below) |
$10 | W | Unknown. crusnexo writes $00000601, $00000641, $00000523 at startup, then leaves as $0000062F during gameplay. thegrid writes $00000601, $00000623, then leaves as $0000062F during gameplay. |
$11 | W | Unknown. crusnexo/thegrid write $000007FF at startup. |
$12 | W | Unknown. crusnexo/thegrid write $00200100 at startup. |
$13 | W | Unknown. crusnexo/thegrid write $00020000, then write to registers $2F and $11, before writing a $00000028 during startup. |
$18 | W | Unknown. thegrid writes $00000000 at startup. |
$19 | W | Unknown. thegrid writes $00000000 at startup. |
$20 | W | Write to a 24-bit sub-register. See the Sub-Registers section below for details. The sub-register number is specified in the upper 8 bits, and the data lives in the lower 24 bits. |
$22 | W | Unknown. crusnexo/thegrid write $00BA9807 at startup. |
$23 | W | Unknown. crusnexo/thegrid write $00654321 at startup. |
$24 | W | Unknown. crusnexo writes $00000601, then a bunch of other registers, before writing $0000000F during startup. thegrid writes $00000601, then a bunch of other registers, before writing $00000001 during startup. |
$2A | W | Unknown. crusnexo/thegrid write $00000000 at startup. |
$2B | W | Unknown. crusnexo/thegrid write $00000000 at startup. |
$2C | W | Unknown. crusnexo/thegrid write $00000003 at startup. |
$2D | W | Unknown. crusnexo/thegrid write $00000070 at startup. |
$2F | W | Unknown. crusnexo writes $0000008C, then writes a bunch of registers, following by writing $0000004C, $0000004C, $0000001C in sequence during startup. thegrid just writes $0000008C and then $0000001C. |
$30 | W | Unknown. crusnexo/thegrid write $0000A18F at startup. |
$31 | W | Unknown. crusnexo writes $000520C4 at startup. thegrid writes $000523C4 at startup. |
$32 | W | Horizontal sync begin (upper 16 bits?). crusnexo writes $002E0015 at startup. thegrid writes $00420015. |
$33 | W | Horizontal blank end (upper 16 bits). Horizontal sync end (lower 16 bits?). crusnexo/thegrid write $0090004C at startup. |
$34 | W | Horizontal total (upper 16 bits) and horizontal blank begin (lower 16 bits). crusnexo/thegrid write $029A0290 at startup. |
$35 | W | Vertical sync start (upper 16 bits?) and vertical blank start (lower 16 bits). crusnexo/thegrid write $0192018F at startup. |
$36 | W | Vertical unknown (upper 16 bits?) and vertical sync end (lower 16 bits?). crusnexo/thegrid write $01B60195 at startup. |
$37 | W | Vertical total (lower 16 bits). crusnexo/thegrid write $000001B6 at startup. |
$38 | W | Wave RAM address of screen base. crusnexo/thegrid toggle between $00000000 and $01900000. |
$39 | W | Unknown. crusnexo/thegrid write $C0FF3010 at startup. |
$3A | W | Unknown. crusnexo/thegrid write $00000000 at startup. |
$40 | W | Wave RAM bank 0 access control. crusnexo writes $00800000 while setting up for access. It writes $00890000 before performing a write access. It writes $00820000 before performing a read access via a manual latch. It writes $00A20000 before performing a read that doesn't require manual latching. Values seen:
|
$41 | W | Wave RAM bank 0 access address and manual latch. Normally, this is the address used for direct accesses to Wave RAM. In some cases (when register $40 is set to $00820000), a write here does not alter the address (the value written is ignored), but rather triggers a read from Wave RAM into the transfer registers at the previously programmed address. When register $40 is set to $00A20000, a write here causes an immediate transfer from Wave RAM into the transfer registers. |
$47 | W | Wave RAM bank 0 write control. crusnexo/thegrid write $0000000F at startup. |
$48-49 | R/W | Wave RAM bank 0 transfer latches. Data that is read from Wave RAM due to an access to register $41 appears here. Data is also written here prior to being pushed out to Wave RAM. Depending on the value of register $4E, writes to this register may actually trigger a flush from the transfer registers to Wave RAM. |
$4C | W | Unknown. crusnexo/thegrid write $00000000, followed by writes to registers $47 and $4F, followed by $3D530000, $3CC90000, $3CC90000, $3C430000 in sequence, followed by a write to register $4D, followed by $000355E at startup. |
$4D | W | Unknown. crusnexo/thegrid write $60990006, then $E0990006 at startup. |
$4E | W | Wave RAM bank 0 mode control.
When performing a write with autoincrement, crusnexo writes $F2080020 at the start, then the address to register $41, then the mode to register $40, then $F2080030, then $F2080069, then does the writes to $48,$49. When finished, it resets the state to $F2080020. Prior to performing writes with explicit address sets before each one, this register is set to $F2080028. Writes are done to $49 then $48. When performing a read with autoincrement, it writes $F2080020 at the start, then the address to register $41, then the mode to register $40, then $F2080030, then $F2080061, then does the reads by doing a dummy write to $41 followed by reads from $48,$49. When finished, it resets the state to $F2080020. Prior to performing reads with explicit address sets before each one, this register is set to $F2080021. Reads are done from $48 then $49. Based on these sequences, the assumption is that the bits mean:
Values seen:
|
$4F | W | Unknown. crusnexo/thegrid write $004F4F00 at startup. |
$50 | W | Wave RAM bank 1 access control. Same as register $40, but for Wave RAM bank 1.
Values seen:
|
$51 | W | Wave RAM bank 1 access address and manual latch. Same as register $41, but for Wave RAM bank 1. |
$57 | W | Wave RAM bank 1 write control. Same as register $47, but for Wave RAM bank 1. crusnexo writes $0000003F here. thegrid writes both $00000004 and $00000001 to control which of two individual pixels to write. Is this a write mask of some sort? |
$58-5A | R/W | Wave RAM bank 1 transfer latches. Same as registers $48-49, but for Wave RAM bank 1. Note that there are 3 latches instead of 2, to account for the 3 words per cell. |
$5C | W | Unknown. crusnexo writes $00000000, followed by a write to register $5F, followed by $FD530000, $FCC90000, $FCC90000, $FC430000, $00620000, $00620000 in sequence, followed by a write to register $5D, followed by $000355E at startup. thegrid does the same thing except the final value written is $0062355E. |
$5D | W | Unknown. crusnexo/thegrid write $44180006, then $C4180006 at startup. |
$5E | W | Wave RAM bank 1 mode control. Same as register $4E, but for Wave RAM bank 1.
Values seen:
|
$5F | W | Unknown. crusnexo/thegrid write $004F4F4F at startup. |
$63 | W | Unknown. crusnexo/thegrid write $C47E0000 at startup. |
$64 | W | Unknown. crusnexo/thegrid write $00000000 at startup. |
$65 | W | Unknown. crusnexo/thegrid write $00000000 at startup. |
$66 | W | Unknown. crusnexo writes $0000008E at startup. thegrid writes both $0000008E and $00000085. |
$67 | W | Unknown. crusnexo/thegrid write $00000000 at startup. |
$68 | W | Unknown. crusnexo/thegrid write $0000009D at startup. |
$6A | W | Unknown. crusnexo writes $43800000 at startup. thegrid writes $43804000 at startup. |
$6B | W | Unknown. crusnexo writes $43480000 at startup. thegrid writes $43488000 at startup. |
$6C | W | Unknown. crusnexo writes $00000009 at startup, later changes to $00000001. thegrid writes $00000009 at startup, later changes to $00000000. |
$6F | W | Unknown. crusnexo/thegrid write $00003FC0 at startup. |
$76 | W | Unknown. crusnexo/thegrid write $44000000 at startup. |
$77 | W | Unknown. crusnexo/thegrid write $43C80000 at startup. |
$78 | W | Unknown. crusnexo/thegrid write $00000000 at startup. |
$79 | W | Unknown. crusnexo writes $C47A0000 at startup. thegrid writes $C7435000 at startup. |
$7A | W | Unknown. crusnexo/thegrid write $000080AA at startup. |
$7C | W | Unknown. crusnexo/thegrid write $003F5F33 at startup. |
$7D | W | Unknown. crusnexo writes $74037808 at startup. thegrid writes $740A7020 at startup. |
$7F | W | Unknown. crusnexo/thegrid write $00000060 at startup. |
Internal Pointer Registers
(copy/paste from Zeus; needs to be updated)
There appears to be a set of internal registers which hold pointers to data in wave RAM. They don't appear to be directly accessible via the register map, but are written to via special FIFO or model commands. Which pointer register is being accessed is controlled by a parameter WHICHPTR. The value of pointer registers is always a block pointer into wave RAM. Below is a table of the values that have been seen so far, and what they mean:
WHICHPTR | Purpose |
---|---|
$008000 | pointer to model data to render via FIFO command $13 |
$018000 | pointer to model data to render via FIFO command $13 (does the top byte matter?) |
$00C040 | pointer to palette to use for texture lookups |
$004040 | set via FIFO command in mk4 (len=02) |
$02C0F0 | set in model data in mk4 (len=0F) |
$03C0F0 | set via FIFO command in mk4 (len=00) |
$02C0E7 | set via FIFO command in mk4 (len=08) |
$04C09C | set via FIFO command in mk4 (len=08) |
$05C0A5 | set via FIFO command in mk4 (len=21) |
$80C0A5 | set via FIFO command in mk4 (len=3F) |
$81C0A5 | set via FIFO command in mk4 (len=35) |
$82C0A5 | set via FIFO command in mk4 (len=41) |
$00C0F0 | set via FIFO command in invasn (len=0F) |
$00C0B0 | set via FIFO command in invasn (len=3F) -- seems to be the same as C0A5 |
$05C0B0 | set via FIFO command in invasn (len=21) |
$00C09C | set via FIFO command in invasn (len=06) |
$00C0A3 | set via FIFO command in invasn (len=0A) |
FIFO Commands
(copy/paste from Zeus; needs to be updated)
Commands are written through the FIFO register ($0E0). There is a whole mechanism in the chip for overflowing the FIFO, returning status in the status register ($0F2) and signalling an interrupt when there is space freed up to continue pumping data. But so far we don't need to emulate that, since we are being simplistic and pretending the whole chip runs infinitely fast.
FIFO commands are a stream of 32-bit data. The top 8 bits (or is it 7 bits?) of the first 32-bit word indicate the command. Some commands require just a single 32-bit word, while others require multiple words to follow.
Several FIFO commands appear to be able to write back to registers. Most registers are written either directly (via accessing memory $880000-$8803FF) or indirectly (via FIFO commands), but a few are accessed in both ways, and match up reasonably well. From what I can tell, only the low 64 registers (from $000-$07F) can be written via the FIFO. This makes sense as the registers higher up are less frequently accessed and appear to be primarily for control.
FIFO Command $00
Bit | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 09 | 08 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Word 0 | $00 | WHICHPTR | ||||||||||||||||||||||||||||||
Word 1 | x | x | LENGTH | x | ROW | x | x | x | COLUMN |
Set an internal pointer register. The low part of the first word (WHICHPTR) specifies which internal pointer to set and probably some mode bits. It is not known if these pointers are associated with registers or if they are only accessible internally. The second word specifies the actual pointer, in block form, providing an explicit LENGTH of 1-64 blocks, and separate ROW and COLUMN addresses.
Model Commands
(copy/paste from Zeus; needs to be updated)
Model commands are stored as a stream of data in Wave RAM. A pointer to the model is specified either through FIFO command $67 or by writing command $00008000 and specifying the pointer to the model data there. The pointer to model data contains a 6-bit length in bits 24-29 and a row/column pointer in bits 0-23. The 6-bit length is the length of the model data in 8 byte chunks, minus 1. Thus, a length of $3F references 64 8-byte chunks.
It is possible that the commands below are related to the FIFO commands (it would make sense). However, there is not a perfect overlap and at least one conflict.
Model Command $08
Bit | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 09 | 08 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Word 0 | $08 | 0 | ||||||||||||||||||||||||||||||
Word 1 | 0 |
Indicates end of model data.
Textures
(copy/paste from Zeus; needs to be updated)
Texture references are a little baffling at the moment. It is clear that a texture is specified by the third word in FIFO command $67 (used by MK4), or in part by a write to register $06 (used by Invasion).
The format of the FIFO command word, based on analyzing the code, appears to be something like this:
Bit | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 09 | 08 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
A | A | A | A | A | A | B | W | W | W | D | C | C | C | C | C | A | A | A | A | A | A | A | A | A | A | A | A | A | A | A | A |
The various 'A' bits seem to go together to form an address of sorts (more on that in a minute).
The 'B' bit's purpose is unknown at this time.
The three 'W' bits control the width of the texture. The width appears to be 512 >> W, giving potential widths of 512, 256, 128, 64, 32, 16, 8, and 4.
The 'D' bit controls whether the texture data is 8bpp or 4bpp. All textures so far appear to be palettized, so a palette base must be set prior to rendering.
The purpose of the 5 'C' bits is unknown at this time.
So, back to the address. The weird thing about texture addressing is that the width specified by the 'W' bits seems to affect the texture address. In essence, if you treat Wave RAM as an array of bytes arranged with a width of 'W', the address 'A' is simply the row number.
W | Width | Wave RAM Address |
---|---|---|
0 | 512(?) | 'A' << 9 |
1 | 256 | 'A' << 8 |
2 | 128 | 'A' << 7 |
3 | 64 | 'A' << 6 |
4 | 32 | 'A' << 5 |
5 | 16 | 'A' << 4 |
6 | 8 | 'A' << 3 |
7 | 4 | 'A' << 2 |