Jump to content
Search In
  • More options...
Find results that contain...
Find results in...
Ladna

Float to Fixed Behavior

Recommended Posts

Converting from floating to fixed point in Doom is essentially:

fixed_t float_to_fixed(float f) {
    return (fixed_t)(f * ((float)FRACUNIT));
}

But this can result in undefined behavior: it's possible for the multiplication operation to yield +INF, and casting that to an integral type is undefined.

 

My question is what Doom expects in this case?  Does it want a clamp at 0xFFFFFFFF or 0x00000000?  Does it expect wraparound?

 

In advance, I'm not interested in "this rarely if ever happens".  I would guess that's the case.  I'm just trying to avoid undefined behavior if I can at all help it.

Share this post


Link to post
23 minutes ago, Ladna said:

My question is what Doom expects in this case? 

Uh, nothing? Doom doesn't use any floating point conversions? Are you talking about some particular source port?

Share this post


Link to post

You have to be very, very careful here. Whatever some Doom port may expect solely depends on the CPU it was developed on.

And there's different behavior here between Intel and ARM CPUs, and on some compilers even differences between instruction sets (Visual Studio will produce different results on non-SSE2 vs. SSE2 enabled systems, even from the same binary!)

 

If you need defined behavior here, check out xs_Float.h in ZDoom, or its wrapper function FLOAT2FIXED. This is basically the only way to guarantee consistent results.

 

Share this post


Link to post

Yeah like basically anything modern.  Like this is in PrBoom+:

static float GetTexelDistance(int dx, int dy)
{
    //return (float)((int)(GetDistance(dx, dy) + 0.5f));
    float fx = (float)(dx)/FRACUNIT, fy = (float)(dy)/FRACUNIT;
    return (float)((int)(0.5f + (float)sqrt(fx*fx + fy*fy)));
}

I imagine Cardboard does similar float -> fixed stuff, or a GL renderer.

 

EDIT: Thanks Graf; I'll have a look :)

Share this post


Link to post

That function has another far more subtle problem:

 

Complex math functions can produce off-by-one-bit results on different compilers.

For renderer-side code this is not relevant but when porting the entire play code to floating point, ZDoom only uses math functions it compiles itself to ensure consistency.

Share this post


Link to post

Shouldn't it be safer to use functions like `floor()` here to convert from float to int? Or multiplication with "1.0" instead of casting from int to float?

Share this post


Link to post

Multiplication with 1.0 and casting is identical. An int -> float cast will always lose the same amount of information, no matter how you do it.

The problem with 'floor' is that they return a float again so you'd still have to cast it back to int and have the same issues to deal with. And the native conversion operations are implementation dependent (so if you need reproducable results, stuff like xs_Float.h is the only viable option.

 

This stuff was actually causing major headaches in some parts of ZDoom's node builder because it only worked on platforms that do a float->int cast with wraparound, but neither ARM nor SSE2 are handling it that way and worse, use different cutoff values. Trying to fix it for one platform would break it even more for the other and vice versa.

 

Share this post


Link to post

To kind of provide closure: I'm planning on doing this kind of thing only for the renderer, so I went with a clamp inside an #ifdef.  It may turn out that I can't confine floats to the renderer, in which case it looks like I'll have to do something like what ZDoom does, but we'll see.  Thanks again everyone.

Share this post


Link to post
6 hours ago, Ladna said:

To kind of provide closure: I'm planning on doing this kind of thing only for the renderer, so I went with a clamp inside an #ifdef.  It may turn out that I can't confine floats to the renderer, in which case it looks like I'll have to do something like what ZDoom does, but we'll see.  Thanks again everyone.

You can always maintain a float for the renderer, and keep the integer var for the logic, insulating yourself from tiny accuracy bugs. This confines any slight math inaccuracies to the renderer, where no one will notice. This has the added benefit that you can avoid having to constantly cast back and forth.

Share this post


Link to post

So here's a question. What source ports provide consistent multiplayer support when someone compiles the same build on multiple CPU's, and which are more prone to desyncs when running different architectures?

Share this post


Link to post

I don't know if a lot of research has been done on this.  Odamex will run on ARM, like a Raspberry Pi, but you probably want to test on wired LAN so as to increase the likelihood that any desync you experience is because of architecture differences and not packet loss.  Chocolate Doom (I think also Crispy) is another candidate, but I don't think you'll have any float-based problems there.

 

I do know that if you goof with the compiler's float flags in Odamex you'll cause desyncs (fast-math; but I can't remember if it should be on or off... probably off), so that indicates vulnerability there, but it's not certain.

Share this post


Link to post

You can't use an asynchronous netcode to test mathematical desyncs in generic cases. Only Doom's original netcode (and demos, as the exact same concept is required) can be used. Thus, Odamex netplay is useless to test such a thing.

 

Safest answer is any port that offers demo compatibility and on multiple platforms usually is fine, but the thing is they are all vanilla compatible ports, so only fixed point math is available. Nobody uses ZDoom to even play back its own demos, and most people only use it on x86 anyway.

Share this post


Link to post

Modern CPUs typically use the same IEEE ruleset when doing math, which means that it is likely that, for a given operation (like a divide), you'll get exactly the same value, to the smallest decimal point. It is when this is not true that desyncs could occur. And, yes, this is less of a potential issue when using fixed-point, which is integer math.

 

Additionally, as Ladna says, compiler flags can let your code "cheat" a bit and not ensure 100% proper math, for performance reasons. And, this is an area where compilers for different platforms will differ in capability. One compiler's "optimize" flags won't necessarily do the same thing on different compilers/platforms. Here, it's best to choose the most conservative option.

 

Share this post


Link to post

Before converting ZDoom to float we did a lot of testing with this.

 

Of the 3 major CPU architectures - 4 if you consider SSE2 and x87 different ones - basic math operations produce the same results (provided that x87 gets set to 64 bit precision internally and you avoid single precision floats due to different precision handling on x87 by different compilers)

The only issues come with CRT-implemented functions like sin, cos and sqrt. Those are implementation dependent and will differ between compilers and architectures. The only solution would be to get a math lib in source form and avoid the CRT's implementations.

 

5 hours ago, kb1 said:

Additionally, as Ladna says, compiler flags can let your code "cheat" a bit and not ensure 100% proper math, for performance reasons.

Needless to say, if you need consistency, it is essential that such flags are all off in code which needs to be reliable. Any such option we tried caused small discrepancies. On MSVC the most conservative option isn't actually needed here, it goes far beyond pedantic.

 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×