Emerald Nova Games

Jo Engine

Math: Integers and Fixed-Point Arithmetic

Most programmers will be used to floating point numbers for calculations. However, the Saturn has no floating point unit. It can support floating points in software, but at the cost of performance. It's best to get used to using FIXEDs when integers won't cut it. To understand how FIXEDs work, we should look at the binary structure of them:

Integer Decimal
32 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

In the above example, the FIXED number represented by that binary sequence is 1 + 1/65536. A FIXED number is a 32-bit number composed of two 16-bit numbers separated by an implicit decimal point between the 16th and 17th bits, (hence, FIXED point number.) The low bits on the right are the decimal or fractionional portion of the FIXED and count up in units of 1/216. Every time you add an integer 1 to a FIXED, you are really adding 1/216 to it. By the time you have added 216 of these to 0, you will have flipped the 17th bit to 1 with all the decimal bits at 0, giving a FIXED value of 1. This makes sense as 216 * 1/216 = 1. The upper bits on the right are a 16-bit representation of the whole number portion of the FIXED functionally equivalent to a signed 16-bit integer. This number rolls over at 215 into negative numbers, however, the decimal portion is always a positive offset.

Integer Decimal
32 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

This is the largest FIXED number, equivalent to 215 - 1 + 65535/65536, or about 32765.99998 (FIXED are generally expressed with 5 decimal places, as that's about all the precision they are useful for.) If we add integer 1 to this we get.

Integer Decimal
32 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Which is equivalent to -32768, the lowest FIXED number. Adding one more gives:

Integer Decimal
32 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

Which is equivalent to -32768 + 1/65536 or about -32767.99998.

To retrieve the decimal portion of your FIXED, you can simply use:

(100000 * (X & 0x0000FFFF))>>16)

The bitwise AND function selects the lowest 16 bits (the decimal portion.) Multiplying by 100000 allows you to extract five decimal places of information, and the bitshift right by 16 bits is effectively dividing by 65536. You can use a formatted print with ".%05d" to display these five digits of the decimal. The integer portion is much simpler.

X>>16

The above converts a FIXED into an integer by effectively dropping out the decimal bits, leaving the equivalent 16-bit integer.

In the general sense, multiplication is handled as:

(X*Y)>>16

However, remember that there is an upper and lower limit to FIXED values, and that mutliplication is handled as if these are 32-bit integers. This means that, while 2 * 8192 = 16384 is less than the maximum FIXED number of less than 32768, the result of mutliplying two 32 bit numbers stored as FIXEDs like that would lead to overflow, as 2 is not 2, but 2 shifted left by 16 bits. The result would actually require representation in bits 33+. A solution would be to first shift the larger number to an integer:

X*(Y>>16)

In theory you could also partially shift both numbers as long as the shifts add up to 16 to keep the decimal in the same place. Generally, you will want to just use jo_fixed_mult(X,Y). This (presumably) stores the product of the two FIXED (32bit * 32 bit = 64 bit) into a pair of 32 bit registers that are treated as an effective 64 bit register. The middle 32 bits are then grabbed as a result, ignoring the high 16 bits (overflow) and the low 16 bits (effectively bitshifting back to FIXED, dropping decimal infromation smaller than 1/65536.) At least, this is what slMulFX(X,Y) does in the Sega Graphics Library. (Jo Engine is built on top of Sega Graphics Library and has access to all its functions.) If you cannot reduce one number to an integer for fast multiplication, it is recommended to use slMulFX(X,Y).

Division gets even more complicated and with greater risk of losing data. It is also much slower (relative to multiplication and especially relative to addition.) Generally, you'd handle division as:

(Y/X)<<16

This is, however, bound to be fraught with loss of information due to the dropping of 16 bits during integer division and then being compensated for with the bitshift back by 16 to return to a FIXED. For FIXED division, use slDivFX(X,Y) for Y/X (from the Sega Graphics Library.) If at all possible, divsion should be avoided for the sake of speed and accuracy. It is popular for distance caclulation and Z-sorting (3D drawing order) to prebuild tables of 1/Z values to multiply by, and simply go by the closest value you wanted to divide by.