Skip to content

Quaternion: refactoring and minor API improvements #2522

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

capdevon
Copy link
Contributor

@capdevon capdevon commented Jun 24, 2025

  • Updated copyright year in the header.
  • Changed import statements to explicitly import only the required classes.
  • Improved parameter and variable naming for clarity in serialization methods (write/read).
  • Fixed method signature style for toAxes() to use Java array conventions.
  • Minor formatting and cleanup for better code readability.

@richardTingle
Copy link
Member

What's the use case here? Quaternion already has getX() methods etc

@capdevon
Copy link
Contributor Author

capdevon commented Jun 25, 2025

What's the use case here? Quaternion already has getX() methods etc

To maintain consistency API with the public variables of the classes Vector2f, Vector3f, Vector4f, ColorRGBA, Matrix3f, and Matrix4f.

@richardTingle
Copy link
Member

Consistency isn't an end in and off itself if it makes things worse. Making these public means we can never change the way quaternion is internally represented

@riccardobl
Copy link
Member

riccardobl commented Jun 25, 2025

I've always though it was a big mistake to make x,y,z,w fields public in the vector classes, in the first place.
Maintaining that for backward compatibility makes things like immutable vectors or transparent double-precision implementations, impossible.

If anything we should figure out how to deprecate the public fields, IMO.

@capdevon
Copy link
Contributor Author

capdevon commented Jun 25, 2025

Yes, guys, I know it's commonly discouraged to access public variables in a class. However, the context changes in graphics engines. Here's why:

Pros (in a Java Graphics Engine Context)

When a class like Vector3f or Quaternion (with public float x, y, z; variables) is heavily used in a Java graphics engine, the primary advantages revolve around performance and readability in mathematical operations:

  • Maximum Performance for Frequent Operations: Direct field access (myVector.x) eliminates the method call overhead (even if minor) that comes with getters and setters. In a graphics engine, where millions of vector calculations can occur per frame, this direct access can translate into a tangible performance advantage in micro-optimization contexts, especially for primitive types.
  • Conciseness and Readability of Mathematical Code: Complex vector operations, such as result.x = vectorA.x * scalar + vectorB.x;, are extremely concise and direct. This can significantly improve the readability of pure mathematical code within the engine, making it more akin to algebraic notation.
  • Simpler Interoperability (Potential JNI): If the graphics engine interacts with native libraries (e.g., for OpenGL via JNI), having public fields can simplify data marshaling between Java and the underlying C/C++ code, as the Java class structure can more closely mirror a C struct.
  • Data-Oriented Nature: In a graphics engine, a vector is often treated as a simple data structure. Exposing fields directly emphasizes this nature, prioritizing efficient data access over stricter encapsulation, which might be perceived as an obstacle to development speed or execution in performance-critical environments.

@richardTingle
Copy link
Member

If it really mattered I'm pretty sure the JVM would inline the getter anyway for performance reasons. But how often do callers really want to get access to the w parameter on a quaternion.

My vote would be to not do this

@yaRnMcDonuts
Copy link
Member

yaRnMcDonuts commented Jun 26, 2025

I'm pretty sure the JVM would inline the getter anyway for performance reasons.

I'm far from an expert in these types of micro-optimizations. (and I do not want to be, if we are being honest)

But from my understanding and a brief searching to refresh my memory, you are correct:

The JVM can inline trivial methods like setX(int x), so the performance difference is often zero in practice.

I nearly always lean towards totally ignoring these types of micro-optimizations because (even in cases where there is a small benefit) they will never actually make a game run faster.

As I often mention, the bottleneck in games is always related to meshes/shaders and the GPU.

And in cases where the CPU is the bottleneck, then it is always related to something major like too many ray casts, and focusing on micro-optimizations (such as a getter vs public fields) is extremely trivial and non impactful.

I personally find this aspect of coding games to be liberating. Games are so big and bloated to begin with, so there's no reason to focus on these micro-optimizations. There certainly are industries where micro optimizations have an impact, but not games. I can sooner save memory or significantly speed up my game by staggering ray casts or reducing pixels in an image. But micro-optimizations will never have any real impact on a game for the end user, so i can freely ignore them and see next to no consequences (this is also why I've mentioned I'm likely not the best to review these types of PRs, but I'm still willing to give my opinions or merge them in cases where there is a proven benefit).

@capdevon
Copy link
Contributor Author

Regarding the proposal to directly expose fields in Quaternion and Matrix3f, I believe this is a fundamental step toward optimizing crucial aspects of our engine and aligning with an already established design pattern.

Core math classes like Vector2f, Vector3f, Vector4f, ColorRGBA, Ray, and Matrix4f already expose their variables via public modifiers. This wasn't an arbitrary design choice; it has proven extremely effective and is widely used throughout jme3-core (especially by Material parsers and ShadowRenderers), as well as by our entire developer community.

All these classes are declared as final. This means they cannot be extended, which makes typical arguments related to inheritance and encapsulation via getters/setters less pertinent in this context. Altering this structure now would mean breaking compatibility with all current and past jME projects—a risk none of us want to take.

Bringing Quaternion and Matrix3f into this consistent design is a logical progression. It's not just about aesthetics; it's about enabling the performance and direct usage advantages I've already detailed, such as reduced allocations and more efficient access. Even without precise measurements, any native optimization can positively contribute to overall performance, which is crucial for achieving high frame rates on less powerful hardware like smartphones or mid-range devices.

The primary benefit of this adjustment is the ability to have direct variable access in high-computation areas such as Material parsers, advanced development tools, and demanding shadow computation functions, where direct field access in our existing math classes is already instrumental and widely exploited.

I understand that strict adherence to getters/setters is generally considered good programming practice. However, in the context of final math classes within a game engine like jME, where the priority is efficiency and the consistency of an already established API, opposing this change purely for formal reasons becomes counterproductive. The consistency with the other six final classes already following this pattern, combined with the tangible benefits in performance and usability, makes this proposal not just valid, but necessary.

@yaRnMcDonuts
Copy link
Member

yaRnMcDonuts commented Jun 27, 2025

in the context of final math classes within a game engine like jME, where the priority is efficiency and the consistency of an already established API,

I actually disagree with this, partly.

The priority is most certainly not efficiency at all costs in a game engine. Engines already accept extremely high computational operations on the GPU that make intensive CPU operations look like nothing. CPU operations that would bog down other time-sensitive apps are barely a drop of water in an ocean when viewed in the context of a computationally expensive game engine.

In a game engine, these minor optimizations that speed up the code by 1/10000 of a second are completely useless.

I feel like I've repeated this many times, but I will say it again because it is very important in context of a game engine and when deciding what to spend your time optimizing:

The bottle neck of a slow game is never going to be inefficient primitive code on the CPU unless the user is already doing something very wrong.

So while it may theoretically be faster to make tiny optimizations to the codebase like this, the fact of the matter is that it will almost never matter in a game engine.


Also I do not think consistency is that important either. Atleast not as a factor in and of itself.

There are often good reasons for doing things inconsistently between classes that otherwise look very similar. Often for good or intentional reasons.

And (to play devlis advocate) I could also raise the point that familiarity is indeed also an important part of keeping code consistent and maintainable. Every minor change like this reduces the level of familiarity for other jme devs who go back and look at a class they previously had experience using. Ther are a handful of jme devs out there who know certain classes or parts of the codebase like the back of their hand, and when we constantly make changes, this reduces that level of familiarity. So it is important that there is a very good and provable reason for absolutely every change that gets made to the engine's codebase.

* @throws IOException from the exporter
*/
@Override
public void write(JmeExporter e) throws IOException {
OutputCapsule cap = e.getCapsule(this);
Copy link
Member

@yaRnMcDonuts yaRnMcDonuts Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that minor changes to variable names like this should be avoided when possible.

I would be just as content with the variable named "oc" or "o" or "outputCapsule" or even "outputCap" and for exception to be "e" or "exc" or "ex" or "exception" and I think that being indifferent to this is a beneficial skill for developers. My own code is full of a mixture of these, and it really depends how I felt that day when I was coding.

I actually strongly believe that it is good practice for a coder to not form a preference in these regards, as it leads to time wasted when things don't fit your preference. And in a room with 10 devs, they will all tell you they prefer it a different way of naming variables and lead to an unproductive debate. So I opt to accept all variable naming formats (as long as they are sensical or spell out the whole word or use acronyms appropriately)

So I'd suggest trying to minimize these types of changes where it doesn't offer much objective benefit, just to keep the review process as fast and focused as possible.

@riccardobl
Copy link
Member

riccardobl commented Jun 27, 2025

All these classes are declared as final. This means they cannot be extended, which makes typical arguments related to inheritance and encapsulation via getters/setters less pertinent in this context. Altering this structure now would mean breaking compatibility with all current and past jME projects—a risk none of us want to take.

I don't agree that removing the final keyword or using sealed classes will break the compatibility

Core math classes like Vector2f, Vector3f, Vector4f, ColorRGBA, Ray, and Matrix4f already expose their variables via public modifiers. This wasn't an arbitrary design choice; it has proven extremely effective and is widely used throughout jme3-core (especially by Material parsers and ShadowRenderers), as well as by our entire developer community.

Please stop with the ai slop, just share a benchmark that proves that it is more efficient to use public fields vs relying on jvm inlining and you'll convince everybody.

I gave it a try:

public field

public final class Public {
    public float x;    
}

public class Test1 {
    public static void main(String[] args) {
        Public p = new Public();
        long t = System.currentTimeMillis();
        for(int i=0;i<2_111_000_000;i++) {
            p.x += Math.ceil(Math.random() * 1000);

            if(p.x%2 == 0) {
                p.x = 0.0f;
            } else {
                p.x = 1.0f;
            }
        }
        System.out.println("Value of x: " + p.x);
        System.out.println("Public) Time taken: " + (System.currentTimeMillis() - t));
    }
}

getter

public class Getter {
    private float x;

    public float getX() {
        return x;
    }

    public void setX(float x) {
        this.x = x;
    }
}
public class Test2 {
    public static void main(String[] args) {
        Getter p = new Getter();
        long t = System.currentTimeMillis();
        for(int i=0;i<2_111_000_000;i++) {
            p.setX(p.getX() + (float)Math.ceil(( Math.random() * 1000)));
            if(p.getX()%2 == 0) {
                p.setX(0.0f);
            } else {
                p.setX(1.0f);
            }
        }
        System.out.println("Value of x: " + p.getX());
        System.out.println("(Getter) Time taken: " + (System.currentTimeMillis() - t));    
    }
}

they run identically

riccardo@riccardo-ws:~/PROJ/DEV/test1$ javac Test1.java 
riccardo@riccardo-ws:~/PROJ/DEV/test1$ javac Test2.java 
riccardo@riccardo-ws:~/PROJ/DEV/test1$ java Test1
Value of x: 1.0
(Public) Time taken: 45839
riccardo@riccardo-ws:~/PROJ/DEV/test1$ java Test2
Value of x: 1.0
(Getter) Time taken: 45590

@richardTingle
Copy link
Member

richardTingle commented Jun 27, 2025

To extend on what riccardobl is saying. AI is certainly a useful tool (I use it myself) but it needs to be guided for a useful purpose (and aggressively checked).

You've produced a lot of changes in a lot of PRs very fast using AI. Some of them have been good, but a lot of them have felt like changes for changes sake and that means review and testing needs to be done for a change that didn't really need to happen in the first place

@capdevon
Copy link
Contributor Author

Thanks for all your feedback, everyone. The widespread "thumbs up" emojis definitely caught my attention.
I want to be clear that my primary goal here is to collaborate constructively to improve the engine and to engage in respectful discussions about different points of view, not to prolong disagreements.

My aim is to contribute positively, regardless of whether a specific PR is accepted. The project isn't mine to own, and if my suggestions don't align, I'm okay with that. We can always continue to exchange ideas and maintain our collaborative spirit.

@riccardobl
Copy link
Member

riccardobl commented Jun 27, 2025

Yeah, i didn't mean to sound too harsh, I think you are doing a good job at passing the engine through ai.
There are likely a lot of small tedious stuff that were overlooked by humans working fast on the project, machines are pretty good at caching this kind of stuff, nowadays. For example this one: #2486, and many other things you fixed, was pretty good (not sure if ai aided in that, but good job nevertheless).

My feedback is just that you should avoid making ai argue for you, most LLMs are too agreeable, they will validate your assumptions even if they are wrong. Or, if you want to share an argument made by an ai that you agree with, please make it write a short summary of the points it is making, and add a disclaimer saying it is ai generated content and from which model it comes, so people can take it with a grain of salt.

If there is a point of contention, IMO, let the code speak (a benchmark in this case).

@capdevon
Copy link
Contributor Author

capdevon commented Jun 27, 2025

I discovered all the bugs related to cloning and serialization by writing tools and demos. Now, I pay close attention to these details because they almost always present a problem.

I only use AI for the analysis and optimization of more complex classes that lack comments or Javadoc. However, I meticulously analyze and verify every code change. I never take anything for granted.

Edit:
I will close this PR within the next 12-24 hours.

@yaRnMcDonuts
Copy link
Member

You could still leave this open for the other parts that are okay (like the formatting, imports and copyright) if you'd like to still update things and submit those parts of this pr (or you can close it if that's your preference)

@capdevon
Copy link
Contributor Author

You could still leave this open for the other parts that are okay (like the formatting, imports and copyright) if you'd like to still update things and submit those parts of this pr (or you can close it if that's your preference)

ok, the protected class variables have been restored.

@capdevon capdevon changed the title Quaternion: making X, Y, Z, and W fields public Quaternion: refactoring and minor API improvements Jun 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants