Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

delta parameter in process() + physics_process() is now f32 instead of f64 #703

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Bromeon
Copy link
Member

@Bromeon Bromeon commented May 6, 2024

For virtual functions Node::process() and Node::physics_process(), this changes parameter delta's type from f64 to f32 in single-precision builds. In double-precision, they stay f64. Effectively, the type becomes real.

Reasons:

  • Reduces the amount of casting boilerplate. Even the cliché example pos += vel * delta currently needs a cast. Most code doesn't benefit from the extra precision.
  • Reduces complexity for beginners; this has come up repeatedly as an obstacle (1, 2, 3).
  • People who need the precision can still use Node::get_[physics_]process_delta_time(), which keeps returning f64.

Closes #510. See there for extended discussion.

@Bromeon Bromeon added quality-of-life No new functionality, but improves ergonomics/internals c: engine Godot classes (nodes, resources, ...) labels May 6, 2024
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-703

@bluenote10
Copy link
Contributor

Isn't it equally confusing to start using real semantics in places where Godot actually doesn't use real semantics? Wouldn't it be strange to have this inconsistency in just this particular case?

My understanding was that Godot tries to use real for space-like things, and double for time-like (but I could be wrong). This kind of made sense, because the spatial stuff is direct input to the GPU, which determines the precision. On the other hand for handling time, 32-bit is often insufficient when mixing time scales on different orders of magnitude.

Of course, this convention can be a bit inconvenient when "space meets time" like in vel * delta, because one is real the other one is double. But making the process's delta a real also feels strange, because it means that one particular time delta deviates from the convention.

@Bromeon
Copy link
Member Author

Bromeon commented May 7, 2024

Is this a distinction that matters in practical scenarios? (I'm not even sure it generally holds).

Looking at real impact, I observe the following:

  1. On one hand, multiple users stumble upon this and get confused. Upon asked why, our answer is no better than "because Godot says so". And here it's noteworthy that several (not all) Godot devs agree that double was a mistake.
  2. On the other, no one came up with a very good argument as to why f64 is needed. It's either "I don't want to lose precision" (valid point, but no concrete use case), "Godot does it" (it's a non-issue in GDScript and C++, plus we have freedom in API design), or "an extra cast isn't that bad" (which isn't even an argument against simplification).

So now that people for whom precision matters have a way to still access f64 via methods, there's no reason for me to keep it.

I need to balance the needs of different users, and believe this is a good compromise, especially since usage gets easier for the majority. Maybe I'm missing something and then we can re-evaluate, but it removes one known ergonomic issue right now 😉

@bluenote10
Copy link
Contributor

Is this a distinction that matters in practical scenarios?

It just feels weird that we now require a cast in a "time meets time" case instead, because we have one (?) instance of a time that is a f32 and all others are f64.

Certainly, such use cases are more rare. They will mainly arise when working with audio, timer, and tweens. Last time I checked, they used f64 pretty consistently, but I'm not sure whether it's true in general either. As an example: Let's imagine we have an audio stream and we want to compute a kind of "progress bar" of an audio player. AudioStream.get_length() returns the time in f64. Without the knowledge that there is a way to get the delta as f64 people will start casting the delta back from f32 to f64 to make it "compatible" with other time-like interfaces. Or worse: Do everything in f32 which could have artifacts from lack of precision.

But I guess what I'm really wondering about is this: The change seems to introduce a general disparity between GDScript classes and gdext classes: Let's say both classes accumulate the delta over time, or print out their delta in each frame for debug purposes. Isn't it confusing that they may not show exactly the same behavior out-of-the-box (when they are implemented in exactly the same way) because the Rust implementation potentially sees different delta values then other classes? I would not expect that when migrating a GDScript to gdext in a 1:1 fashion.

But I guess I'm pretty alone on this so optimizing for the majority use case is fine, and I can work around it on my end.

@Bromeon
Copy link
Member Author

Bromeon commented May 8, 2024

These are good points indeed. I think you briefly mentioned this in #510:

I haven't looked in detail for which interfaces the full precision might be particularly relevant (mostly timing related things come to mind),

But this sounded very vague and not like a workflow you'd actually use -- so, has that changed? Also, you stated that an extra line wouldn't bother you much, which would be the same situation after this change? 😉


Maybe in the end one of the more "magic" approaches is better if we want to support both use cases equally. Or we support Vector2/Vector3 scaling operations that accept f64. Or we have a process64 additional function, etc.

My problem is that I don't have the data points on how many people actually use the f64 precision, but I do know that several people have an issue with the status quo, and I've noticed the friction myself. So it might be that this change is not final or not the best approach, but leaving f64 without trying any alternative could mean we miss an opportunity to improve ergonomics.

@StatisMike
Copy link
Contributor

@Bromeon Is it possible to add documentation for generated process and physics_process methods? If so, I think it would be useful to incorporate information about the workaround you've specified:

People who need the precision can still use Node::get_[physics_]process_delta_time(), which keeps returning f64.

@coder137
Copy link

@Bromeon Will this be a breaking change? For existing physics_process and process methods that use f64?

@Bromeon
Copy link
Member Author

Bromeon commented May 12, 2024

@coder137 Yes. It would technically be possible to add a deprecation magic within the proc-macro; I'd need to see how much effort it would be.

Out of curiosity, how do you currently use process/physics_process?

@coder137
Copy link

coder137 commented May 13, 2024

@Bromeon I don't use process/physics_process a lot so refactoring should be fine even if it is a breaking change.

Mostly in cases of movement/rotation (where I do delta as f32) or if I need to need something that ticks once per game loop I end up discarding delta.

Since delta is always small, in this case delta as f32 will always succeed. Is it necessary to make the change from double to float?
Casting in rust is explicit, but that just comes with the language. If godot has delta as double I believe we should leave it at that.

@Bromeon
Copy link
Member Author

Bromeon commented May 14, 2024

Is it necessary to make the change from double to float?

The reasoning is in the first message and the issue being closed by this PR; I'd like to not reiterate it 😉


Casting in rust is explicit, but that just comes with the language.

The point here is that the cast is not only unnecessary for a majority of cases, but it's provably confusing (see the various people who stumbled upon this obstacle on Discord). What matters most to me is user experience and efficient workflows. More than "idiomatic Rust" for example. See also Philosophy.

@Bromeon
Copy link
Member Author

Bromeon commented May 25, 2024

It just feels weird that we now require a cast in a "time meets time" case instead, because we have one (?) instance of a time that is a f32 and all others are f64.

@bluenote10 This is not true btw, the Godot API is quite inconsistent when it comes to representation of times.

Just some random samples:

Method Type
Animation::get_length f32
AnimationPlayer::get_blend_time f64
AudioEffectDelay::set_feedback_delay_ms f32
AudioStream::get_length f64
AudioStreamGenerator::get_buffer_length f32
CpuParticles3D::get_lifetime f64
ENetPacketPeer::set_timeout i32
Input::get_joy_vibration_duration f32
OggPacketSequence::get_length f32
SpriteFrames::get_frame_duration f32
Timer::get_wait_time f64

So even within the same domain, Godot sometimes uses different types.

My conclusion here is:

  • It's not possible to make everything consistent. We shouldn't aim for a perfect solution.
  • We should try to make the common cases easy to use, because that benefits more users and reduces entry complexity.
  • We should also try to reduce the chore imposed by having separate f32/f64 types, however that may look.
  • Vector algebra is imo very common, almost everyone needs to update positions in a game.

Now that doesn't mean changing process parameters to f32 is the only way. The alternative I see is to make geometric scaling operators work with both f32 and f64, e.g. Vector2 can be multiplied/divided by both types.

@bluenote10
Copy link
Contributor

@bluenote10 This is not true btw, the Godot API is quite inconsistent when it comes to representation of times.

Indeed that looks rather messy, thanks for collecting some examples!

Now that doesn't mean changing process parameters to f32 is the only way. The alternative I see is to make geometric scaling operators work with both f32 and f64, e.g. Vector2 can be multiplied/divided by both types.

I've been thinking about this some more as well. In principle that is what GDScript does as well when multiplying scalar floats (which are f64) with Vector2 / Vector3, right?

What would be the main argument against that solution? That a repeated cast has an unnecessary performance overhead? From a practical perspective I guess the probability of observing any measurable effect of casting a scalar several times instead of just once is pretty low. Perhaps it is sufficient to document on Vector2 and Vector3 that they support all scalar operations of f32 and f64 for convenience, but if a user wants zero overhead they should use the real type for their scalars exclusively.

Does it justify a feature flag? Normally feature flags are opt-in, but here we would have a case of opt-out for the better default, so I'm not sure not if a feature like strict_vector_operators would make sense.

@Bromeon
Copy link
Member Author

Bromeon commented May 26, 2024

What would be the main argument against that solution?

Nothing per se, but it can be a bit confusing since Rust's scalar types don't allow it.

So you can write

f64 * Vector3 * f32

but not

f64 * f32 * Vector3

Well possible that it's not a big issue in practice 🤔


Does it justify a feature flag? Normally feature flags are opt-in, but here we would have a case of opt-out for the better default, so I'm not sure not if a feature like strict_vector_operators would make sense.

I don't think so, it's a very niche thing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: engine Godot classes (nodes, resources, ...) quality-of-life No new functionality, but improves ergonomics/internals
Projects
None yet
Development

Successfully merging this pull request may close these issues.

f32 vs f64 in Godot APIs
5 participants