Ladna Posted July 16, 2017 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. 0 Quote Share this post Link to post
Linguica Posted July 16, 2017 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? 0 Quote Share this post Link to post
Graf Zahl Posted July 16, 2017 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. 0 Quote Share this post Link to post
Ladna Posted July 16, 2017 (edited) 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 :) Edited July 16, 2017 by Ladna 0 Quote Share this post Link to post
Graf Zahl Posted July 16, 2017 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. 0 Quote Share this post Link to post
fabian Posted July 17, 2017 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? 0 Quote Share this post Link to post
Graf Zahl Posted July 17, 2017 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. 0 Quote Share this post Link to post
Ladna Posted July 17, 2017 (edited) 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. Edited July 17, 2017 by Ladna 0 Quote Share this post Link to post
kb1 Posted July 18, 2017 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. 3 Quote Share this post Link to post
Danfun64 Posted July 21, 2017 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? 0 Quote Share this post Link to post
Ladna Posted July 22, 2017 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. 0 Quote Share this post Link to post
Edward850 Posted July 22, 2017 (edited) 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. Edited July 22, 2017 by Edward850 0 Quote Share this post Link to post
kb1 Posted July 27, 2017 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. 0 Quote Share this post Link to post
Graf Zahl Posted July 27, 2017 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. 0 Quote Share this post Link to post
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.