Fixed point
Normally you'll be working with integer numbers (1, -25, 123, etc.), but sometimes you'll want to be able to use in-between values (for example, measuring speed in whole pixels is a bad idea because then you'll be stuck with multiples of 60px per second!).
Modern CPUs use floating point to handle non-integer numbers, but that's too slow on the 68000. Instead, we'll learn of another approach that's much faster on old hardware, called fixed point.
What's fixed point?
The idea behind fixed point is that instead of 1 = one unit, you use a larger number to mean one unit, and smaller numbers refer to sub-unit amounts. For example, if 1000 = 1 unit, then 250 = 0.25 units (because 1000 × 0.25 = 250).
The most common fixed point format on 68000 is "16.16" (i.e. 16-bit
integer part, 16-bit fractional part). This is because it allows for
some optimizations taking advantage of the 68000's ability to handle
both 16-bit and 32-bit values. In this case, one unit = 65536 (or
$10000
in hexadecimal).
Iwis says
The name fixed point comes from the fact that the "decimal point" is always in the same place (contrast with floating point where the precision of the fractional part changes depending how large is the number).
Basic math
The following operations are exactly the same for integers and for fixed point, so keep using them the same way you were already doing:
- Copying values around (of course)
- Addition, substraction, comparison
- Anything binary (AND, OR, bit shift, etc.)
This remains true for both signed and unsigned, as well.
Converting to integer
To get the integer part of a fixed point number, you divide the value by one unit… normally, you'll make it so that 1 unit is a power of two, which means you'll be able to do a bit shift right instead.
If you followed the suggestion to use 16-bit integer and fractional parts, it's even easier:
- Reading from memory as 16-bit gives you the value as an integer
- Reading from memory as 32-bit gives you the value as fixed point
The above not only is easier to program for, but also it's faster than doing bit shifts.
Iwis says
In many cases you'll want to stick to the integer part for things where sub-unit precision isn't that important, e.g. checking collision between two objects (where pixel precision is just fine).
Rounding to nearest integer
The method mentioned above always rounds down, i.e. towards negative. This is best for performance and in many cases it's good enough for what you want (or outright what you actually need), but maybe you'd prefer to round towards the nearest whole number instead.
To do this, take the original number, add 0.5 units, then strip down the fractional part (following the format above, add 32768 and then drop the bottom 16 bits).
Multiplication and division
Multiplication and division become more problematic, because they cause the location of the decimal point to shift! Ideally you should try to avoid them, but if you can't:
- To do A × B = C, first do the multiplication and then divide by "one unit". Note that the intermediate value will be larger than it normally takes in memory, so make sure to not truncate the result!
- To do A ÷ B = C, first you need to multiply A by one unit (again, don't truncate!), then proceed to do the division. Do not mess with B or C, leave those as-is as they're already in the correct size.
If you're using the 16-bit integer + 16-bit fractional part format, then you can divide by one unit by doing a bit shift right by 16. This said, you'll be still dealing with large multiplications and divisions (which are harder and much slower to do on the 68000), so you really still want to avoid this unless absolutely needed.
Also incidentally, if the second operand (i.e. B) is integer, it becomes simpler since you can skip the "divide by one unit" step in both cases. In that case use B as-is (i.e. without converting to fixed point).