doom-engine
id Software have released most of their game’s core engine code under the GPL. This provides a great opportunity to take a look at how things were done ‘back in the day’ - even though I’m sure some concepts still apply today. I have cherry-picked a few areas of interest.
Fixed-point arithmetic
Back in day (he says) it seems floating point operations weren’t terribly efficient and it was much faster to use integers (so I’m told). To get around those limitations, developers came up with the concept of fixed-point arithmetic. Instead of having, say, a 32bit int representing an integer, it was divided in x.y parts with x representing the integer portion and y the floating one. Doom used a 16.16 format. So that meant having an integer range of +/- 32767/8 respectively (2^15 + 1 bit for sign), and floating point accuracy of 1/65536 (~0.000015).
So let’s take the signed 16.16 case and how it might work in practice:
...
hexadecimal:80010000
unsigned int:2147549184
integer part:-32767
floating part:0
...
Let’s lump the above into some macros instead and write some tests!
Quick note: if you’re having issues with macros, try compiling using gcc -save-temps
and look at the .i file generated - it will show you how macros have been replaced in your code.
Now that we’re satisfied the macros work as expected, we can take a look at operations on fixed point numbers.
Providing we’re dealing with the same fixed point format (eg, 16.16 vs 16.16), addition and subtraction work as expected:
Again, note the loss of precision - this is because the fractional part is not a multiple of 2^n - if we used 100.128 instead, we’d see what we expect.
Multiplication and division are a bit more fun. Using fixed point arithmetic, we’re essentially scaling numbers - meaning the scale factor will get multiplied/divided along (see 2
for a really nice explanation). The m_fixed.c
file of the doom engine shows us how it’s done. Notice how multiplication ‘scales back’ the result by FRACUNIT, or how division scales it up (in the section commented out, which doesn’t handle division by 0).
That concludes the introduction to fixed point arithmetic. Hopefully that’s enough to make some sense out of m_fixed.c
and m_fixed.h
!
Further writing: Lookup tables for trigonometric functions - why and how they’re used