What techniques have people successfully used or can suggest to deal with a consistent cross platform math for procedural world generation? Also, if you have done this, what were the pros and cons of your approach? In particular, I'm trying to decide whether to go with a lightweight fixed point approach, or a use a heavyweight multi-precision library [I went with the lightweight approach].
Context: I'm building a game (in C++) that involves generating a 400 billion star galaxy, for which I'm using procedural generation. The target platforms are Android (Arm-v8a), Linux (x86-64), and probably Windows (x86-64). The issue is that this generation involves quite a bit of math, including exp, pow, sqrt, sin, cos, tan and atan2, and the system is ill-conditioned (tiny differences in the calculations could propagate to large differences in the output, like a star turning up or not, or even a whole region of stars changing). I want the same seed to generate exactly the same galaxy on all platforms in case I ever want a multi-player version of this.
My reading so far is that different processors, compilers or libraries cannot be guaranteed to return the same results for transcendental functions, and even simple floating point math might be a problem (such as Arm using a multiply-add instruction having better intermediate precision than the x86-64 using two instructions).
Things I have considered:
Using double precision floating point, including the same transcendental function code to generate the same results (which I've had to do anyway so that I can vectorize them), and tweaking the compiler flags so that everything compiles exactly the same way on both processors, and rely on IEE-754 conformance to take care of the rest. [this part answered. It's nearly impossible to get right]
Using a lightweight fixed point framework such as fixedptc.h so I'm only doing integer math. Pros: will definitely be consistent. Cons: lots of effort to scale everything correctly, particularly as generating a galaxy requires numbers with a large dynamic range that is not normally suitable for fixed point.
Using a heavyweight multi-precision framework such as GNU MPFR or Boost (which I think is built on something like MPFR. Pros: this is easier to use than a lightweight library. Cons: This is a lot of machinery to pull in to solve this, and I haven't found any guarantee that it is completely consistent cross platform.
Example of the sort of places where dynamic range can be a problem (although it turns out that in this case much of it can be precomputed and doesn't need to be in procedural generation):
absolute star luminosities can range from about 430,000 times the mass of the sun to 1/10,000 the mass of the sun (that's not including planets and moons, they will have to have their own scale), that's a factor of 4,300,000,000, which can be represented in a 32.32 fixed number, but calculations before or after risk overflowing or underflowing.
positions and distances store fine in a 64 bit integer, but when for instance working out how far from a spiral arm a point is (which affects the star density), it would be helpful to work with distance^2 to avoid expensive square roots, and squaring a number doubles the amount of dynamic range required - this may require a different scale factor for the generating a large elliptical galaxy like IC 1101 than it would to generate something like the Pleiades.
None of this is insurmountable, but it certainly requires a lot more than blindly replacing doubles by m.n fixed integers.
Notes:
I'm not particularly concerned about performance, or even high accuracy. Most of the heavy math code is not called very often, and I can use a lightweight approach for the critical stuff.
I'm not trying to get the rendering the same, as that appears to be a fools errand with all the different graphics cards out there, different screen resolutions, etc. I do want the underlying structure to be the same though.
Generating it on one platform and storing it is not a solution - the galaxy is too big for that.
This question is not asking about general cross-platform development and frameworks, about which I know there are many opinions (I've already made the major decisions for my case), just about getting consistent math for procedural generation.
I'm not trying to solve the space scaling issues - I've already solved this (with for instance 64 bit integers for the star positions). I'm dealing with star distributions and properties which involve a bit of math.
RESULT: There is no one true answer, even in a single project. There seem to be four properties to think about when determining how to perform calculations on continuous quantities (things that would be represented mathematically with real numbers): accuracy, (cross platform) safety, speed and dynamic range.
double has great accuracy, speed and dynamic range, but is not safe.
float has great speed and dynamic range, but is not accurate or safe.
fixed point has great speed and is safe, but not so good accuracy or dynamic range. More bits extends the dynamic range but slows things down. Either hand roll or use a lightweight library for this.
multi-precision libraries like MPFR or crlibm have high accuracy and dynamic range, but are not fast, and I haven't found anything definitive on safety. (crlibm provides correctly rounded results, so it should be safe).
Looking at my project as an example:
There are a lot of constants that can be pre-generated even without the seed, and the dynamic ranges get truly ugly here (I really don't want to do fixed math on calculations involving the speed of light squared and plank's constant). This needs accuracy and safety. For this I'm going to use doubles, but have a checksum or generate it statically on a known platform for safety.
For the shape of the galaxy (spiral arms, sub-clusters etc.), all we need is safety and moderate dynamic range. Fixed point here (although multi-precision libraries might be ok), with a lightweight library for transcendental stuff.
For star placement, we need safety, speed and some dynamic range (the dynamic range of luminosity can be factored out of this step). 64 bit fixed with occasional forays to 128 bits seems to be good here, although care needs to be taken with overflows and underflows. I'm using fixedptc.h for the sqrt stuff and rolling the rest myself.
For star rendering, we need speed and dynamic range. floats are the way to go here, it's what low end graphics cards work with anyway.
(later) for planet placement, we need safety and probably dynamic range. This will probably be the time to break out one of the multi-precision libraries.
This problem is now solved as far as I'm concerned (thanks for some really useful suggestions!), and I've done the bulk of the transition to fixed. Any further edits/comments are to leave ideas for anyone coming later who is trying to deal with the same issues.
Postscript: I found there were some places where fixed point had insufficient range (mostly in the setting up phase), so I ended up writing a small library to provide the floating point functionality I needed using integer instructions. See my answer below for details.