23

Some classic vector arcade games, such as Battlezone and Red Baron, were based on the 6502 CPU. The processor wasn't fast enough to do the 3D computations, so a co-processor commonly known as the "math box" was used for vertex transformations.

How was the Math Box programmed from the 6502? What API did it provide? What were the performance limitations?

  • I've updated my answer; now it includes almost all of the API. (At least, until I discover another part of the API...) – wizzwizz4 Dec 2 '16 at 21:28
  • Consider accepting secondperson's answer. It's almost complete (unlike mine). – wizzwizz4 Jul 11 at 15:58
4

Here are my findings:

Disclaimers:

  • This answer is entirely based on mame's mathbox.cpp source and assumes it is correct.
  • This answer focuses on succinct high-level description, even at the cost of being imprecise. (which it is)
  • Despite this, this answer currently misses the high-level intent behind some of the operations. (needs some help from someone more graphics-math-minded?)
  • All names are made up.
  • No warranty etc etc - parts of the answer may be mistaken or incorrect.

notes:

  • All registers/operations are signed unless specified otherwise.
  • Internal registers (REGc..REGf) are ignored by this answer.
  • "8.8 fixed-point" means a 16-bit value whose upper 8-bits contain the whole part and lower 8-bit contain the fractional part e.g. 0x1234 represents 0x12 + (0x34 / 0x100)

The mathbox consists of some 16-bit registers:

  • a result register
  • 12 user-facing registers REG0..REG11 (and some internal ones)

The mathbox has 3 8-bit read addresses:

  • read status byte (0 means done)
  • read low byte of result
  • read high byte of result

And 32 8-bit write addresses:

  • 00 - write low/frac byte of REG0, set result to REG0
  • 01 - write high/whole byte of REG0, set result to REG0
  • 02 - write low/frac byte of REG1, set result to REG1
  • 03 - write high/whole byte of REG1, set result to REG1
  • 04 - write low byte of REG2, set result to REG2
  • 05 - write high byte of REG2, set result to REG2
  • 06 - write low byte of REG3, set result to REG3
  • 07 - write high byte of REG3, set result to REG3
  • 08 - write low/frac byte of REG4, set result to REG4
  • 09 - write high/whole byte of REG4, set result to REG4
  • 0a - write low/frac byte of REG5, set result to REG5
  • 0b - write high/whole byte of REG5, then do sub-then-cross-product
  • 0c - write low byte of REG6, set result to REG6
  • 0d - write low byte of REG10, set result to REG10
  • 0e - write high byte of REG10, set result to REG10
  • 0f - write low byte of REG11, set result to REG11
  • 10 - write high byte of REG11, set result to REG11
  • 11 - write high byte of REG5, then do divide-products
  • 12 - do dot-product
  • 13 - do divide-REG89
  • 14 - do divide
  • 15 - write low byte of REG7, set result to REG7
  • 16 - write high byte of REG7, set result to REG7
  • 17 - set result to REG7
  • 18 - set result to REG9
  • 19 - set result to REG8
  • 1a - write low byte of REG8, set result to REG8
  • 1b - write high byte of REG8, set result to REG8
  • 1c - write high byte of REG5, then do window-test
  • 1d - write high byte of REG3, then do diff-max-plus-3/8-diff-min
  • 1e - do max-plus-3/8-min
  • 1f - unknown (selftest or signature analysis?)

Operation descriptions:

sub-then-cross-product:

  REG4 -= REG2
  REG5 -= REG3
  do cross-product

cross-product:

  // inputs are 8.8 fixed-point
  // interpretable as 2d vectors (REG0, REG1) and (REG5, REG4)
  REG7 = REG0 * REG4 - REG1 * REG5
  result = REG7
  // REG7 receives whole part of result, fractional part goes nowhere

dot-product:

  // inputs are 8.8 fixed-point
  // interpretable as 2d vectors (REG0, REG1) and (REG5, REG4)
  REG8.REG9 = REG0 * REG5 + REG1 * REG4
  result = REG8
  // REG8 receives whole part of result,
  // REG9 receives fractional part - but with its lowest bit forced to 0
  //
  // WARNING: going by the mame source, this operation can continue and do 
  // a divide-REG89 as well (also changing the result register) 
  // if internal register REGf is not -1.
  // (Things like calling sub-then-cross-product or any divide  
  //  will set it to -1, but reset & window-test will likely set it
  //  to some other value.)

divide-REG89:

  // REG8 should be the high/whole part of dividend, 
  // REG9 - the low/fractional part. 
  // REG7 is an integer
  result = REG8.REG9 / REG7, using a size of REG6 (see below)

divide:

  // REG11 should be the high/whole part of dividend, 
  // REG10 - the low/fractional part. 
  // REG7 is an integer
  result = REG11.REG10 / REG7, using a size of REG6 (see below)

divide's REG6 explanation:

  • REG6 controls the size of the division as follows:
  • if REG6 is set to 16, then the full 32-bit dividend is divided by the divisor, resulting in a 16-bit result.
  • if REG6 is set to 8, then only the middle two bytes of the two-word dividend comprise an 8.8 fixed-point dividend, though the other bytes should be 0 (lowest byte) and sign-extension (highest byte) to get a correct result, which is in 1.8 fixed point.
  • other REG6 values (<= 16) seem to act similarly, making up a REG6.REG6 fixed-point dividend, "centered" between the dividend's two words. (Other bits should be 0s and sign-extensions), with the result being in 1.REG6 fixed point.
  • (a lower REG6 is faster)

divide-products:

  // effectively, if REG0,REG1 and REG5,REG4 are 2D vectors
  // this operation computes their (dot-product + REG3),
  // divided by their (cross-product + REG2)
  // (TODO: there's probably a better explanation of what this actually achieves)

  do cross-product // -> REG7
  do dot-product // -> REG8.REG9
  REG7 += REG2
  REG8 += REG3
  REG9 &= 0xff00 // mask lower byte (presumably to allow REG6 being >= 8)
  do divide-REG89 // this sets result to the division result

window-test:

  // likely related to computing midpoint-subdivision for line-clipping, or a similar algorithm (thanks @fadden)
  do (REG6 + 1) times:
  {
    midx = (REG4 + REG7) / 2
    midy = (REG5 + REG8) / 2
    if (midx > REG11 && midx > midy && midx + midy >= 0):
    {
      REG7 = midx
      REG8 = midy
    }
    else
    {
      REG4 = midx
      REG5 = midy
    }
  }
  REG6 = -1
  result = REG8

diff-max-plus-3/8-diff-min:

  REG2 = abs(REG2 - REG0)
  REG3 = abs(REG3 - REG1)
  do max-plus-3/8-min

max-plus-3/8-min:

  result = max(REG2, REG3) + (3/8) * min(REG2, REG3)

Thanks for Tommy and fadden for pointing out that the complex math is most likely meant to be vector math.

  • 1
    "window-test" might be midpoint subdivision, used for line clipping. – fadden Jul 10 at 22:14
  • 1
    @fadden that's exactly what I thought; I also think the stuff about complex numbers is a misdirect: it looks more likely to be used as a vector dot product, which you'd use to apply a transformation to a vector. E.g. it'd directly do the 2d rotations of Asteroids, and if you did it twice you could apply an arbitrary 3d transform. – Tommy Jul 10 at 23:52
  • @Tommy: I agree about the complex numbers. The docs at jmargolin.com/uvmath/uvmenu.htm discuss using the dot product for surface normals, but make no reference to quaternions. I haven't figured out what the deal with computing 3/8ths of things is though. – fadden Jul 11 at 0:06
  • @ fadden & @Tommy - yeah, vector math does make more sense than complex numbers, I was thrown off by the register order, but if the points are given by (REG0,REG1) and (REG5,REG4), then the complex-number multiplies do become vector dot/cross product, which does make a whole lot more sense for 2d games. I'll edit. – secondperson Jul 11 at 0:59
  • One more guess: 3/8ths might be to do with the aspect ratio for final output? I've no great confidence in this guess; I'm of course taking it as given that it wasn't worth implementing 4/8ths because you hardly need a maths box for that. – Tommy Jul 11 at 3:43
18

The 6502 accesses the Atari Math Box, via a pre-defined memory map and address decoder,[1§I] as a memory-mapped I/O device.[1§IV] The top four bits of the External Address Bus are sent to the address decoder to determine whether to activate the high-score module, the POKEY module or the Math Box for a given operation.[1§IV]

The bottom five bits of the External Address Bus are sent to ROM A1 in the Math Box, which calls one of 32 instructions accordingly.[1§IV.A][2#87-297] The Math Box communicates on the bi-directional External Data Bus,[1§Fig. 4] where the Math Box's four AM2091 chips' input and output lines are connected.[1§IV.B] There are 16 registers,[2#14-29] which are 16-bits each.[Inferred from [2]]

Writing to memory addresses 6080 -609F (inclusive) starts the Math Box clock and runs the specified routine.[1§IV] The lower five bits of the memory address will determine the lower five bits of the External Address Bus and thus the instruction that will be executed. This is the mapping between the five-bit instruction number and the instruction executed:

  • 0x00: Sets the low byte of register 0x00 to input, then returns register 0x00.[2#89]
  • 0x01: Sets the high byte of register 0x00 to input, then returns register 0x00.[2#90]
  • 0x02: Sets the low byte of register 0x01 to input, then returns register 0x01.[2#91]
  • 0x03: Sets the high byte of register 0x01 to input, then returns register 0x01.[2#92]
  • 0x04: Sets the low byte of register 0x02 to input, then returns register 0x02.[2#93]
  • 0x05: Sets the high byte of register 0x02 to input, then returns register 0x02.[2#94]
  • 0x06: Sets the low byte of register 0x03 to input, then returns register 0x03.[2#95]
  • 0x07: Sets the high byte of register 0x03 to input, then returns register 0x03.[2#96]
  • 0x08: Sets the low byte of register 0x04 to input, then returns register 0x04.[2#97]
  • 0x09: Sets the high byte of register 0x04 to input, then returns register 0x04.[2#98]
  • 0x0a: Sets the low byte of register 0x05 to input, then returns register 0x05.[2#100]
  • 0x0b: ?
  • 0x0c: Sets the low byte of register 0x06 to input, then returns register 0x06.[2#103]
  • 0x0d: Sets the low byte of register 0x0a to input, then returns register 0x0a.[2#112]
  • 0x0e: Sets the high byte of register 0x0a to input, then returns register 0x0a.[2#113]
  • 0x0f: Sets the low byte of register 0x0b to input, then returns register 0x0b.[2#114]
  • 0x10: Sets the high byte of register 0x0b to input, then returns register 0x0b.[2#115]
  • 0x11: ?
  • 0x12: ?
  • 0x13: ?
  • 0x14: ?
  • 0x15: Sets the low byte of register 0x07 to input, then returns register 0x07.[2#106]
  • 0x16: Sets the high byte of register 0x07 to input, then returns register 0x07.[2#107]
  • 0x17: Returns register 0x07.[2#117]
  • 0x18: Returns register 0x09.[2#119]
  • 0x19: Returns register 0x08.[2#118]
  • 0x1a: Sets the low byte of register 0x08 to input, then returns register 0x08.[2#109]
  • 0x1b: Sets the high byte of register 0x08 to input, then returns register 0x08.[2#110]
  • 0x1c: Window test?[2#248-263]
  • 0x1d: ?
  • 0x1e: Set register 0x0d to max(register 0x02, register 0x03) and set register 0x0c to min(register 0x02, register 0x03), then increment register 0x0d by (3/8 times register 0x0c), then return register 0x0d.[2#278-288]
  • 0x1f: Some sort of self-test or signature analysis?[2#290-294]

No function loads the low byte of register 0x05 without performing a computation.[2#101] No function loads the high part of register 0x06 at all,[2#104] so it could be overwritten during certain functions. I assume that the input is one byte, because the MAME implementation's[2] repeated bitshift-and-XOR makes more sense that way.

  • I'm not sure how the input / return relates to the API in the above text, but might work that out soon. If anyone else can work out what any other part of the code does, please comment and / or edit this answer and / or post your own answer. – wizzwizz4 Dec 2 '16 at 21:16
  • @secondperson Please do! I could barely find any information online; if you know more, then please please write up an answer. – wizzwizz4 Jul 10 at 19:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.