-
-
Notifications
You must be signed in to change notification settings - Fork 217
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
Refactor vector_macros to consistently generate all vector types #721
Conversation
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-721 |
8ae92ef
to
56e0753
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the PR!
May I ask what the motivation behind this refactoring is? As mentioned in the Contribution guidelines, it would be good if larger changes were briefly discussed beforehand, to make sure our ideas are aligned and no work is wasted.
I haven't implemented all the functions yet because I wanted to get some feedback first, but when I'm done
glam-rs
should be able to be removed.
I'm not sure if this should be a goal. glam is relatively lightweight and battle-tested. Implementing all its functionality on our side not only means reinventing the wheel, but it comes with some increased maintenance and bug risk (albeit little, but our testing is lacking too). We may also lose out on potential optimizations that glam is doing -- either now, or in case we decide to use Vec3A
or similar APIs.
Your benchmark indicates that the new implementation is faster in many cases. Did you run it in release mode? If yes, what do you think is the bottleneck of the existing implementation -- maybe indirections through glam2
etc? Do you see any change if you #[inline(always)]
those?
I'm not sure if moving the associated constants to macros is a good idea -- from user/API perspective, this is an objective downgrade, as the documentation comments are now very generic (basically not expressing more than the name already does). I don't think there's a big problem with duplicating them 🙂
On a positive side, I think reducing duplication between the vector types is nice if it doesn't come with worse docs or significant complexity increase (thinking of things like strip_leading_plus
, which can be OK in isolated instances but not too much 🙂 )
/// Returns the length (magnitude) of this vector. | ||
#[inline] | ||
pub fn new_length(self) -> real { | ||
(strip_leading_plus!($( + (self.$comp * self.$comp) )*) as real).sqrt() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess $( (self.$comp * self.$comp) )+*
doesn't work?
Also, as real
should be replaced with RealConv
functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the +
gets interpretet as any number of repetitions but at least one and you can't escape it.
I don't quite understand how I should be replacing as real
with RealConv
, since the return type is also real
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do you need as real
here actually? does rust just not understand what the type is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The impl_vector_fns
macro implements functions for floating-point and integer vectors. Because the result of strip_leading_plus!($( + (self.$comp * self.$comp) )*)
for integer vectors is i32
and sqrt
doesn't exists on i32
it needs to be casted. Since there is no performance impact for floating-point vectors i didn't see the benefit to splitting this, as the only difference would be the type casting.
Since this has the potential of having a huge diff, it would also be nice if you could separate it into multiple PRs, to accelerate the review process:
(But maybe let's finish the discussion on the overarching idea first 🙂 ) |
Sorry, I was browsing the project and saw #310, experimented a bit and then got carried away. Next time I will definitely do it. My main goal in moving everything into macros is to make it easier to add new methods and maintain existing ones. Since they ussaly only exist once. I don't feel strongly about removing glam but I don't necessarily see the benefit of using it's API as it sometimes diverges from godots.
Small improvements come from eliminating the overhead of converting to glam and back, larger ones come from faster implementations based on godots implementations (limit_length vs. clamp_length_max) or making use of the procedural aspect of macros. Since I based everything on godots API, I would propose that I add integration tests to all functions.
Only |
It'd be strange if Would you be able to clean up the data a bit more? Maybe remove any results that have no measurable difference, make sure functions are inline properly, and include how big the performance improvement is for each? As is it's not all that easy to read all the data. |
I forgot to tell godot to use the release build and I misinterpreted what functions to inline. Sorry about that. So yes, most of the improvements disappear when inlining the existing function and running in release mode. Except for cases where the godot implementation is faster. |
You're right, I think they're good -- I remember Godot's docs being confusing for some constants, but it was probably somewhere else. Thanks for the deduplication! 👍
So that brings us back to the motivation of this PR. Performance can make sense in the cases you mention. But maybe we should look at those individually, based on benchmark results? If inlining improves perf over current Removing glam is where I tend to be against. After thinking a bit more about it, there are several reasons why I'm not convinced it's a very good idea:
Of course, general improvements such as adding Summary: to move this forward, I would suggest:
Does that sound good? |
I went through all function benchmarks in release mode and inline the functions where I found performance differences. Even though the differences in performance are small, they are the same for every benchmark run. length
normalized
since
Based on these findings I would suggest to only rewrite I would continue as follow and create multiple commits for all these steps if given the ok:
Some functions currently only exist on some vector types and not in Godot (e.g. |
I think that makes sense. glam does this: pub fn normalize_or(self, fallback: Self) -> Self {
let rcp = self.length_recip();
if rcp.is_finite() && rcp > 0.0 {
self * rcp
} else {
fallback
}
} I'm not sure how Btw, we should likely change how we do our benchmark, because these numbers are subject to huge error.
This has almost no expression power. It's unclear if it's the difference between 0.49 and 0.51 ns, or between 0.001 and 1.4... Anyway, this is out of scope for this PR.
They seemed like a good idea at the time, but yes, feel free to remove If there are other functions you want to remove, please mention them explicitly. It's easy to miss something in the diff otherwise. |
Are there any additions/removals, or other semantic changes compared to the I'm against adding additional Regarding move-to-macro, I'm not sure if we benefit much from doing this for everything. For methods/constants/operators it can help keep things consistent, but I would not do it for the 3 traits you mentioned. Otherwise it becomes really annoying if we want to customize one of them. In general, code duplication is only a problem where it actively hurts maintenance -- we shouldn't go on a crusade based on principle 😉 But yes, other than that your suggestion sounds good 🙂 feel free to use separate commits if it helps clarity. Thanks a lot! |
56e0753
to
fc83bb2
Compare
This is the Based on this refactor i would gradually refactor the vectors. |
Thanks! And sorry about the conflicts, I was also refactoring a small part around the |
fc83bb2
to
3ec1fda
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot! Also for the doc improvements all over the place 👍
I made some comments.
/// Returns the axis of the vector's highest value. See [`Vector2Axis`] enum. If all components are equal, this method returns [`Vector2Axis::X`]. | ||
#[inline] | ||
#[doc(alias = "max_axis_index")] | ||
pub fn max_axis(self) -> $crate::builtin::Vector2Axis { | ||
use $crate::builtin::Vector2Axis; | ||
|
||
if self.x < self.y { | ||
Vector2Axis::Y | ||
} else { | ||
Vector2Axis::X | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes semantics and signature. Current implementation is:
pub fn max_axis(self) -> Option<Vector2Axis> {
match self.x.cmp(&self.y) {
Ordering::Less => Some(Vector2Axis::Y),
Ordering::Equal => None,
Ordering::Greater => Some(Vector2Axis::X),
}
}
It's relatively easy to do .unwrap_or()
on user side, but gives more control about the fallback than the Godot impl.
Handling equality may be especially relevant for the integer vectors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since f32
doesn't implement cmp
should the implementations be split or would this be sufficent:
pub fn max_axis(self) -> std::option::Option<$crate::builtin::Vector2Axis> {
use core::cmp::Ordering;
use $crate::builtin::Vector2Axis;
match self.x.partial_cmp(&self.y) {
Some(Ordering::Less) => Some(Vector2Axis::Y),
Some(Ordering::Equal) => None,
Some(Ordering::Greater) => Some(Vector2Axis::X),
_ => None,
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that should be good enough. The caveat is of course that with float imprecision, exact equality is unlikely as soon as arithmetic is involved, but I think that's fine.
Btw, since the macros are not user-facing but instantiated locally, you don't need to fully qualify all the symbols. Just import them at the beginning of the file -- that makes things quite a bit easier.
What I was wondering also is, should we document how we can get the Godot behavior of max_axis_index
using this function? With unwrap_or
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about?
/// To mimic Godot's behavior unwrap this function with`unwrap_or(Vector2Axis::Y)`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds great, thanks!
Tiny adjustment (also space/comma):
/// To mimic Godot's behavior, unwrap this function's result with `unwrap_or(Vector2Axis::Y)`.
Btw, maybe wait with resolving conversations that are still active 🙂
pub fn snapped(self, step: Self) -> Self { | ||
Self::new( | ||
$( | ||
(self.$comp as real).snapped(step.$comp as real) as $Scalar | ||
),* | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need an as real
now?
Previous implementation:
gdext/godot-core/src/builtin/vectors/vector_macros.rs
Lines 425 to 429 in 4f9f47f
pub fn snapped(self, step: Self) -> Self { | |
Self::new( | |
$( self.$comp.snapped(step.$comp) ),* | |
) | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous implementation was only for float vectors. Since self.$comp
can now also be a i32
and snapped
only exists on real
it needs to be converted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think code deduplication should come at the cost of introducing floating-point operations for integer vectors. This is a potential issue for precision and performance.
Maybe it can be implemented separately? Otherwise omit snapped
for integer vectors, for now...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think its possible to implement it any other way since snapped
requires division and Godot's implementation is the same. But I can omit it for now.
Btw. while looking at Godot's snapped
implementation I noticed that it produces different results to the real
implementation
((self / step + 0.5) * step).floor()
vs (self / step + 0.5).floor() * step)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can gladly correct our float impl to match Godot's.
Regarding integer impl, something along this lines? Didn't test much, so almost certainly bugged, but it should be possible without float operations. Division doesn't imply floating point. We could of course look at existing impls as well...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think its possible to implement it any other way since
Or I'm simply not smart enough😅
Didn't test much, so almost certainly bugged
Only found that it breaks when steps are negative
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, are negative steps supported in Godot? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell, yes e.g. if you snap 7 to 2, you get the higher possible result (8 and not 6) but with -2 you get the lower on (6 and not 8).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Godot documentation doesn't mention negative values, but they are definitely supported by design and not by accident. For a long time: I found godotengine/godot#6399 from a time where issue numbers were still 4 digits... 🏹
Maybe we could test for a few values that the inner function (generated) returns the same result...
3ec1fda
to
f120e7d
Compare
Could you maybe rebase this onto |
f61e21e
to
c00663b
Compare
Should I push the other vector refactors or would you like to review the commits individually? |
You can gladly push more commits on top. Would be nice however if you could fix the merge conflicts (should be a small one this time), and if CI is green -- that way, the docs can be generated and we can inspect the results in the link from the GodotRust bot 🙂 |
2323e19
to
b21f9ee
Compare
ba217ea
to
9d93265
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR looks mostly good, a few small comments remaining.
Are things ready from your side, or you plan to do more changes? As mentioned, unrelated improvements can also be applied in separate PRs. Would be cool to merge this soon 😎
/// Performs a cubic interpolation between this vector and `b` using `pre_a` and `post_b` as handles, | ||
/// and returns the result at position `weight`. `weight` is on the range of 0.0 to 1.0, representing the amount of interpolation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this could also get a 1-line-brief description 🤔
9d93265
to
7f2e5ca
Compare
75ac810
to
bba7b35
Compare
I expected |
bba7b35
to
064511f
Compare
Replacing |
For posterity:
100% agree that Godot's behavior is better. See also previous discussion. |
Fixed the failing test for the Thank you so much for this PR -- it's a great enhancement to consistency, documentation and correctness! 💪 |
Could #310 be updated? Since, except for |
Done! ✔️ |
I have reworked
vector_macros
so that all functions and constants that are available in godot are also available in rust for all vector types.I have grouped functions and constants depending on which vector types they can be found on.
For all functions for which an implementation was already available, I added benchmarks and checked if both return the same result. I then named the new functions
new_*
.I haven't implemented all the functions yet because I wanted to get some feedback first, but when I'm done
glam-rs
should be able to be removed.