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

Non-Mutating Vectors #731

Closed
wants to merge 6 commits into from
Closed

Non-Mutating Vectors #731

wants to merge 6 commits into from

Conversation

bjornbytes
Copy link
Owner

TLDR; all vectors are permanent, vector methods no longer mutate, it's a big breaking change under consideration.

This pull request resolves #169 by doing the following:

  • All vector objects are now "permanent" garbage collected objects. For now the 2 different constructors (e.g. lovr.math.vec3 and lovr.math.newVec3) both exist for backwards compat, but they both do the same thing.
    • They are full userdata, the hacky approach using the lightuserdata was removed.
  • Vector methods that previously mutated their first argument now instead return brand new vector objects with the result. The only exception to this are the :set methods, which continue to assign the vector's components using various inputs.
  • Because of the previous change, the add/sub/mul/div methods became redundant with metamethod operators, and so they were removed.
    • Similarly, "constructor" methods like Mat4:orthographic/perspective/fov/lookAt/target are a bit weird now. Those should probably instead be lovr.math.orthographic or Mat4.orthographic instead of taking an unused Mat4 object as the first argument.
  • To avoid some measured performance issues with creating too many vectors, it is now possible to declare temporary vector scopes using lovr.math.push and lovr.math.pop. Vectors created inside the scope will be invalidated once the scope is popped, allowing them to be reused across loop iterations, frames, etc.
    • Note that the :set method can be used inside a temporary scope to persist values, since the usual a = a + b approach wouldn't work because it assigns a temporary vector to a.

During the development of this branch, a version using FFI was attempted but abandoned because it was about 4x slower than using Lua's C API, for non-benchmark projects.

This is a massive breaking change that will basically break all existing code! There is a large cost to merging this and it is still under consideration. Feedback is welcome.

- Removes temporary vectors.  All vectors are permanent now.  The
  temporary constructors still exist for compatibility, but they create
  permanent vectors.  Also removes lovr.math.drain.
- All vector methods (except :set) return new vectors instead of
  mutating.  This makes some methods vestigial/weird, like
  :add/sub/mul/div and matrix "initializers" like
  :identity/:fov/:perspective/:ortho/:lookAt/:target/:reflect.
@nensanders
Copy link

First, thank you again for providing this excellent code base.

I've identified three primary use cases for LOVR, each with its own specific needs:

Rapid Prototyping and Fun Experiments:

The API should prioritize user-friendliness and accessibility. The goal is to enable quick and easy experimentation without the need for deep technical understanding.
Performance enhancements should not come at the cost of complexity or 'magic' in the API, as this could hinder the learning and experimentation process.

Community Contributions and Packages:

This aspect involves creating libraries that simplify or empower the first use case.
It's about enabling those who understand the underlying mechanics to build tools and abstractions. These tools can help others who may not yet grasp the full technical details, fostering a supportive and educational community environment.

Scriptable Rendering Pipeline for Advanced Projects:

In scenarios like mine, where the core simulation would be handled in C/C++, the primary requirement would be to interface with math types without overhead.

Since I am very new to LOVR, I cannot really estimate the impact of the change.
My impression is that the change is possible because people mostly used it for "throw away" projects in the past.
In my experience the adoption rate for non-prototype kind of projects correlates strongly with the maintainablity of the framework, because in the extrem case I would need to maintain it myself.

I understand making a decision about the proposed pull request involves
estimating what the cost of change is and the benefits on the other side:

Is cost of change low because framework user activity is low?

What mainly drives your decision making on where the framework is heading?

@bjornbytes
Copy link
Owner Author

bjornbytes commented Jan 24, 2024

Hey, thanks for the thoughts. I think the assessment about the use cases is accurate. LÖVR aims to be:

  • Easy to use for beginners to sketch things out without much code (as well as for people quickly prototyping ideas)
  • Unopinionated about high-level concepts. Libraries are used to solve common needs, or for solutions that require a high degree of customization.
  • Flexible/powerful enough for complex projects that want to do things "from scratch"

As far as deciding on how to make a breaking change, it's really difficult!

Since #169 was opened a few months after vectors got added, there has been consistent feedback from people that LÖVR's vector API is confusing because the methods mutate instead of returning new vectors. Separately, people seem to trip up on the 2 different types of vectors: permanent vs. temporary. Temporary vectors also use a pretty nasty hack involving the global lightuserdata metatable that I've never been super proud of. So there have been a lot of people asking for this change, and good reasons to do it.

On the other hand, some people liked vectors the way they were and felt that the mutating API, temporary vector pool, etc. were important for performance. Sure, you have to copy vectors explicitly when you need a new one, but this control lets you really cut down on the number of objects you create and reduces GC impact.


This branch was an attempt to experiment with switching to a non-mutating, easier API like people have wanted and A) try it out to see how it "feels" to use and B) measure performance. Initially there were some pretty big performance regressions, but these were solved (without adding back temporary vectors). Regarding performance, my conclusions from recent testing are:

  • Most of the overhead of vectors is due to calling into Lua's C API for every vector method.
  • LuaJIT is really fast at garbage collection and for a lot of projects the cost isn't going to matter.

Based on this, I'm not sure that the temporary vector system and mutating API was hitting the right tradeoff! It made things more confusing for everyone and although the API reduced the amount of garbage created (when used well), in real projects this wasn't impacting performance all that much and the user friendliness of the API was more of a bottleneck. During the development of the initial vector API, I was a little bit too cautious about garbage collection.


And finally, regarding the breaking change part: Although LÖVR isn't at 1.0 yet, and there isn't a huge number of people using it, LÖVR does try to avoid making changes like this. In recent versions features have been deprecated instead of removed, and I plan on doing this moving forward whenever it's sane to do. We are still arguably recovering from the graphics rework in v0.16.0 when LÖVR switched to Vulkan, and making everyone rewrite their vector code is kinda just salt in the wounds. I wouldn't say the cost of making this change is low, at all.

However, people have also pointed out that making this change earlier is better than waiting until later (when there's even more code to port), and it does fix something that people have been criticizing for a few years now.

@bjornbytes
Copy link
Owner Author

This probably won't be merged soon because #737 brought up more questions about vectors that need to be resolved.

@bjornbytes bjornbytes closed this Apr 19, 2024
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.

Do cross, normalize, etc. need to mutate the original vectors?
2 participants