Improving Float ergonomics (Proposal)

(Marco Fruhwirth) #21

Yes, you are right. I was thinking of f32 values and not of results of computations. Especially for time this might be important, as the time delta might often be an infinite fraction.

(Andrea Catania) #22

Yep exactly

(Théo Degioanni) #23

I have also heard that a type alias would require twice as much testing on merge, which is a load for CI and also a developer might discover the f64 while submitting their PR and seeing that it does not build.

In my opinion, those arguments are not very strong however: twice as much CI is, well, just a flat cost. And regarding CI failing for unaware contributors, in my opinion it is better they discover it that way than not at all.

(Andrea Catania) #24

Good point

(Andrea Catania) #25

A solution to this problem could be follow what the guy of NPhysics did.
They define a generic type during the initialization of the world here -> https://github.com/rustsim/nphysics/blob/master/src/world/world.rs#L22 and they use it for their calculations.

This could require more rewrites than the type alias, but could be a good alternative.
In case we would try to do something similar to this, I propose to shift to that direction one step at a time in order to not put all the load of rewriting on to the back of a single developer and on a single PR.

(Théo Degioanni) #26

The reason why generics should not be used here is that it creates different flavors of the same component which can be hard to maintain. For example, fetching a storage for Transform will not fetch a storage for Transform. Although maybe with default type parameters we can move the burden to the few people using f64. Is there any other reason why generics were discarded (apart from build time maybe?).

(Paweł Grabarz) #27

The goal of Float was never “consistency” of any kind. We never wanted to put it everywhere in the engine. That’s not the point. It is simply a tool for situations when you MIGHT want extra precision in SOME specific situations. This is pretty much only about supporting large game worlds where 32bit floats aren’t enough for object positioning. There are still uses for f32 and f64, also I would say use them unless you have a really good reason to use Float in your particular case.

So, Floats serve a single purpose: to allow switching on expensive but very much needed precise world space transforms.
Also Floats are designed to hold some extra constraints:

  • to make serialization/future prefabs easy, to not leak complexity outside of Transform type itself and to not introduce errors when requesting resources, we can’t use generics. Those are just way too error-prone in context of primitive rust floats. Compiler is trying to be too smart, guess too much and often gets it wrong.

  • the cargo check outcome (pass or fail) has to be independent from the float64 feature. We don’t want to double our CI test duration, but also don’t want to introduce a new kind of tricky errors, the ones you see only after you’ve commited your code and made a PR. It’s already there for some features, but those are fairly isolated cases. Float type has too much reach for that to be allowed.

  • flag can’t change public API. This links to above, but is more general. We don’t want to break interop with libraries by accident, and that would be way too easy with type aliases.

And now to address some extra points directly:

NPhysics uses “RealField” generic, as nalgebra does. You don’t create those types too often, and even then the contexts where you use them actually benefits from rustc default behaviour. This is not true for amethyst.

You will not loose precision when you do math with f32s. Your “Timer” might be f32, upcasting it to f64 tells you that only your “Timer” data alone is less precise than it could be now, but you use full precision of your transforms and you store it in the full precision afterwards. You never discard any bits.

Bringing examples from other languages about using aliases or typedefs as a solution isn’t really productive. Our solution and actually problem itself is very Rust specific, and in fact based very nuanced behaviour. What worked somewhere else doesn’t necessarily applies here. Reverse is also true. If any other language doesn’t behave like rustc specifically does, then likely similar solution to ours is just bad.

Also just because your Transform is in particular precision, it doesn’t mean you have to always treat it like that. We will likely adapt camera-relative positioning eventually, and that will always be f32. That way we can keep many systems working in f32 (notably rendering) without much precision loss. Reverse is also true: you might want to calculate your physics in f64 and just store the result with lower precision, as the increased precision of your intermediate steps of calculation might help reducing the simulation error significantly.

3 Likes
(Jaynus) #28

After a discussion with @Frizi, I’ve been asked to post my thoughts on this. In a nutshell, I think we should get rid of the entire Float newtype entirely and shift back to a 32-bit default Transform.

Although the solution is elegant for the way it is written, I think the entire 64-bit Transform was written as a solution to a problem that doesn’t exist, that we really have no way of measuring what a “good” solution is.

Said another way, aside from “Wanting 64-bit precision for lage worlds”, we have no actual solid use cases defined to write a solution for. Because of that, the entire solution was written as an engineering challenge which ended up with the solution we have today. I don’t think it actually solves anyones problems current; and I think it actually even causes more issues. This isn’t at all a insult in any way on @Frizi or @jojolepro, please dont take it that way.

I just think this was an optimization that was best left for later, and needed a lot more discussion before solutions were banged out. In the game engine world, 64-bit precision for transforms is actually a MUCH more niche issue than generally talked about; theres a reason Unity, UE4, and others stick to f32 - the benefit of sticking to what your graphics pipeline demands outweighs the work required to deal with precision issues when they come up.

There are niche games and scenarios where 64-bit precision his useful. I propose we provide for them a set of 64-bit precision functionality to use, selectively and separately, which would then integrate with the existing 32-bit engine.

That means Transform64 with Transform64System, Parent64System and Animation64System respectively. These should be opt in, and include added overhead of using them, which is that values have to be transformed and copied between Transform and `Transform64 (which we will provide the mechanism to do that).

In regards to the above mentioned overhead, incase anyone didn’t know, the current solution actually imposes a a set of type-conversion overhead because of the rendering pipeline, that everything has to be cast/transformed down to 32-bit anyways to render. This is a hardware limitation that won’t change anytime soon. Rather than imposiing this invisible overhead, we should make explicit overhead opt-in for 64-bit users.

Finally, I’d just like to say that many cases where people say “I want 64-bit precision for my super-large game world” actually dont want 64-bit precision for their suer-large game world; they actually don’t know what they need or want, but a few extra decimals, most of the time, won’t actually solve what is actually a technical design problem.

There are many solutions to those problems: Camera-centric rendering, World Origin offsets, chunked worlds, hierarchies and other solutions. All of these need to be considered for large worlds, and I think imposing the amount of ergonomic headache we have on users for what people may not even need has turned out to be a bad choice.

13 Likes
(Gray Olson) #30

Do you have a specific use case, i.e. user story, that is relevant to an actual project that exists in amethyst which needs 64-bit floats? I completely agree with @jaynus’ analysis that we don’t really have a solid one and as such should really hold off and try to come up with better solutions to this problem, or focus our efforts on implementing things like camera-centric rendering/chunked worlds/etc. as strategies that could be easily adopted in a client’s game.

(Andrea Catania) #32

This game engine is made specifically to solve a special use case, and the double precision is properly implemented.

What we are trying to do here is a mitigation of the problem, and again having only the transform with double precision doesn’t mean that we will have the same benefit described by this game engine.

For this reason I don’t think that worth bloat the code with Float any longer.

1 Like
(Joël Lupien) #34

Let’s remove it in this case

(Paweł Grabarz) #35

I really like the idea @jaynus come up with. I think at this point it’s safe to say we just want to go with it.

I’ve created two issues, one for Float removal and second for bringing f64 precision back using systems designed to actually solve specific problems.


2 Likes
(Thomas Schaller) #36

From the issue:

This might require a generic argument in the implementation, but in that case we should expose two type aliases with specific types filled in.

That sounds like a good idea. Maybe we can end up with the following:

pub struct GenericTransform<T> { .. }

pub type Transform = GenericTransform<f32>;
pub type Transform64 = GenericTransform<f64>;
pub type TransformFixedPoint = GenericTransform<FixedPoint>;

What do you think? This way, we can use concrete types and avoid the whole literal coercion (/ wrong default parameters), while not duplicating much code.

(Théo Degioanni) #37

But then the types would leak, and people could be tempted to use the wrong version.

I too think jaynus’s proposal is what we should do.

(Thomas Schaller) #38

I do like @jaynus’ version, but we need to implement different precisions somehow. GenericTransform is long enough, and we can just link to Transform from there.

If you make completely seperate types you need to define traits with a lot of methods, or else we need to duplicate all the code dealing with Transform. And the trait solution still requires duplication inside the Transform.

(Khionu Sybiern) #39

I remember @Rhuagh stating something about not doing a physics engine without f64?

(Jaynus) #40

Yes - most physics engines will speak about only using 64-bit precision because of simulation issues. Something to keep in mind generally, however, is that physics engines tend to live in their own little silo’ed world.

No physics engine uses our ECS data structures directly (because ecs is horribly optimized for physics), and they all maintain their own world independent on amethyst (and the renderer). In standard engine design, this physics world is an isolated silo of data, which is periodically polled to sync with the simulation world.

This means we are discussing costs, really, on the type conversions at different stages of the engine. Would we rather:

  • Incur conversion overhead every render frame, from simulation world (f64) to render (f32)
  • Incur conversion overhead every physics sync, from physics world (f64) to simulation world (f32)

From a practical performance and design standpoint, option #2 is ideal - as this happens independent of user/player visibility, at regulated times, and incurs a known fixed explicit cost. We were previously doing #1, which came with hidden implicit costs.


Now, this is not to say there aren’t targets for optimization which could have minimized the cost of f64 internal to the engine. We currently have many things that can benefit from caching, target-specific instruction sets (AVX component iteration? yes please!) and other changes. But from a general use perspective, I think the ergonomic cost and our time is better spent in other ways of supporting large game worlds which are even better than just 64-bit precision.

3 Likes
(OvermindDL1) #41

Didn’t read in-depth, but I am planning on using amethyst for my next play project so a few notes…

Large ‘world’, I often run with positions as f64, even though the GPU eventually gets f32 all transforms around the camera remain f64 because once they are transformed around the camera it lowers to f32 and precision remains, otherwise the great great majority of the game area tends to start getting very very jumpy when moving away from origin. This is an issue a few games have had as well, f32 inaccuracies in even not-so-large worlds can make the graphics and movement a bit… unusual when decently far from the origin.

However, for my truly ‘large’ area games (I have a tendency to do space games…) I do tend to ‘chunk’ it up in fairly small chunks, each chunk has an i64/i128 x/y/z(/w) positions (most of them procedurally generated on demand) and inside each chunk I use f32 coordinates (usually constrained to 32k units, as more than than has incurred floating point inaccuracies to cause gameplay to suffer, though often chunk sizes can be a lot less if there are many potential renderables within). Each chunk is essentially a ‘parent’ for everything that exists within it that is otherwise unparented, and the chunks are generated transformed position relative to the camera location.

So f32 transforms would work for me as long as everything can have a parent transform so I can move it from its original i64/i128+f32 positions to bring it to ‘within’ just f32 space easily. This includes being able to do multiple renders for a scene so I can overlay ‘far’ things transformed/scaled down to render “large” far things as normal (which the rendering pipelines should be able to handle easily enough if my reading is correct).

1 Like
(Lokathor) #42

For large worlds you usually want to chunk the world up or you use fixed point. Just going with f64 instead doesn’t help nearly as much as either of the alternatives.

(Kel) #43

f64 can be good for jitter when you orient the relative space for computations around a good origin. fixed point is great for a lot of things as well