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

add option to generate jars with baked in mixins #1583

Open
noah1510 opened this issue Oct 6, 2024 · 9 comments
Open

add option to generate jars with baked in mixins #1583

noah1510 opened this issue Oct 6, 2024 · 9 comments
Labels
enhancement New (or improvement to existing) feature or request

Comments

@noah1510
Copy link

noah1510 commented Oct 6, 2024

graalvm has support for compiling java programs to the native platform in a feature called native image.
There have been several people that got this working for vanilla minecraft but so far these projects don't work for modded minecraft mainly because of mixins.
Mixins don't work in native code because most of the class information is gone as it got turned into native byte code.
This proposal is about adding a flag that generates binaries that have the mixins baked into them, so that they don't have to be reapplied on every launch again.
This might improve the game load time especially when a lot of mods are installed and it allows generating a native image using graalvm, as the runtime mixins are already applied.

It is already possible to output the patched classes using the -Dmixin.debug.export=true flag.
This generates a .mixin.out dir in the .minecraft dir with all of the mixed in java byte code classes.

My idea would be to add a flag to first dump all the bytecode in that dir (excluding all mixins and mixin plugins) and then override the classes as the mixins get applied.
The mod loader should then make sure to run all the mixin plugins and apply all the mixins early, to spit out the patched bytecode.
All the assets and data dirs should be extracted as well and be turned into a giant resource and asset pack that can be loaded in one go as well.

In a second step the user can turn that output dir into a really big jar, that can be ran directly and has all the mod code in it.
This second step might need some mod loader changes to work correctly as this patched code must not try to load code from the mods dir, nor from the minecraft library dirs.

Since I am not a java dev some feedback on this is very appreciated.
In theory many parts of this can be done easily by just extracting the jars as zip into the .mixin.out (doing it recursively for the jar in jar files)
and then ignoring the META-INF dir and removing the top level files (mixin configs and pack config).
While this process would result in a way slower first start time, following game launches could be significantly faster.

@noah1510 noah1510 added the enhancement New (or improvement to existing) feature or request label Oct 6, 2024
@noah1510
Copy link
Author

noah1510 commented Oct 6, 2024

I looked a bit more into the tools that exist for the neoforge build process.
JavaSourceTransformer and MergeTool should be able to do most of the things I suggested here.
All that is missing is a way to run those tools with all ATs and Mixins from all installed mods during the runtime.

@heipiao233
Copy link

In (Neo)Forge, all the transformations (including Mixin) of class files must go through ModLauncher. Therefore, ModLauncher can be modified to export the transformed class files and load them directly if the mod list is not changed.
I think we can add a method in ILaunchPluginService, such as generateCacheSign. If the signature returned by one of the services is different from the last launch, rebuild the cache. For example, we can return the mod file hash values for generateCacheSign in FML.

@noah1510
Copy link
Author

noah1510 commented Oct 6, 2024

That architecture makes this a lot easier.

Mods can mixin/AT other mods right? In that case once any mod gets updated the whole cache has to be rebuilt, unless all transformers get indexed and hashed).

@IThundxr
Copy link
Contributor

IThundxr commented Oct 6, 2024

If i am understanding this correctly, you are saying that modified classes should be built into mods so they can be used instead of mixins being applied (basically mixins being done at compile time)

However this would not work in the event where multiple mods are mixing into the same classes as who's bundled class should be applied? Mod A's or Mod B's?

@noah1510
Copy link
Author

noah1510 commented Oct 6, 2024

No not at all.

The idea is that on the first launch of the game all mods apply their transformations and all of the modified classes get cached and saved to the disk. (maybe even add a launch flag to just do this step then close the game)

On the following game starts these cached class files then get used and all source transformation steps can be skipped.

Every time a mod gets updated or added the whole class cache needs to be rebuilt but that is still way less often than doing it on every launch.

The goal is to speed up the game load times on slower CPUs and to potentially allow graalvm native image generation for even better performance gains. This doesn't actually do something the mod loader isn't doing at the moment, this is just about reusing the intermediate output.

@HenryLoenwind
Copy link
Contributor

There's one caveat I don't see a solution for: One of the core concepts of how modding is set up is that classes are loaded conditionally and on demand. This means that in a typical installations there may be hundreds of classes that won't classload. Trying to convert everything that's in a modded installation into class file that can be processed would run into countless classloading error---with no way of knowing which ones are real errors and which ones are classes that would never be loaded in the current setup.

@noah1510
Copy link
Author

noah1510 commented Oct 7, 2024

I am not that familiar with the (neo)forge mod loading structure so the assumptions in this are made based on the way fabric works.

The modded game launch happens in roughly the following stages:

  • start the mod loaded
  • find out what mods are installed and their versions
  • create a list of all static transformations (ATs & mixins) from mod config files
  • run the mixin plugin to retreive conditional extra mixin class names
  • create a map with class names and all the transformations that get applied on it
  • start running all the game and mod init classes (I know this is several step but not important for this)

Correct me if I am wrong but my understanding is that at the moment all the transformations are applied every time an indexed class gets loaded.
Even if the transformed source is saved to memory, this is less than ideal because many systems have high memory pressure in modded minecraft.

My proposal is to at the very least store all the transformed class code on disk after the transformation was applied and then let the class loaded load that transformed source instead of applying the transformations again.

More ideally there should be another step in the launch:

  • ...
  • create a map with class names and all the transformations that get applied on it
  • apply all indexed class transformations and store the result to disk
  • start running all the game and mod init classes

This class transformation cache needs to be tagged with the combined information of all installed mods and their versions.
Once anything changes the things needs to be rebuild.

If there is no change in the mod list the whole scanning for mixins and ATs can be skipped and we can just assume that the cache on disk is a complete list of all the classes after they were transformed.

In the long run it might be worth considering exporting all sources to that cache.
This way the mod loader no longer needs to keep track of which classes are transformed and which aren't and just load any class from the disk cache.
This should reduce the class load time by even more and would reduce the memory usage as well.

Note that none of this would require knowledge over which classes would be actually loaded or not.
It is still the same code that would get executed it just adds a caching step.
This does make the assumption that every class that gets a mixin registered could be loaded at some point, but this assumption is already being made and might already cause issues.

@TheSilkMiner
Copy link
Member

Some additional points to consider with this:

  • Are transformers comparatively slower than IO? As in, would this caching even improve performance enough where this actually makes sense? If, after all of this, all you gain is 0.1 ms, then I'd doubt the whole process is worth it. We'd need some actual profiling data.
  • Is the process of computing the cache key, verifying if it matches, and all faster or slower than no cache? Essentially, suppose that the cache key is a hash of every mod loaded: would computing that be faster than just assuming a miss?
  • Memory pressure is a non-issue: you need to hold the transformed classes in memory anyway, because that's where code has to reside. Transformers might be a valid point, but I doubt that's even close to a bottleneck on any system that is running MC.
  • Transformers might perform queries that might be completely unrelated to your loaded mods (e.g. querying the GPU you're using, or something like Sinytra might load mods from some random unrelated place, or whatever cursed stuff Essentials does).
  • What's the size on disk of this potential cache?
  • This would require essentially loading every single class that might or might not be loaded by the game or by every mod ever. Suppose a mod has an optional dependency and that class would never be loaded under normal circumstances without the dependency being present. Now if a transformer accepts that class you have to load it. How would you handle such an error case (because ASM might require info from classes that don't exist)?
  • The end goal here seems to be GraalVM Native Image, which I'd argue is a NeoForge non-goal. With this I'm saying it would be nice, but AFAIK Neo's policy is to ensure stuff works correctly with HotSpot and in particular MS's flavor of it that the game ships with. Everything else is out of the Mod Loader's immediate scope.

@HenryLoenwind
Copy link
Contributor

Correct me if I am wrong but my understanding is that at the moment all the transformations are applied every time an indexed class gets loaded.
Even if the transformed source is saved to memory, this is less than ideal because many systems have high memory pressure in modded minecraft.

I'm not sure I understand what you're trying to say here. "Source" as in "the raw bytes of the class file as loaded from a jar"? That is not saved in memory. That is transformed and discarded. The result of the transformation is given to the JVM and then discarded, too.

"time an indexed class gets loaded" makes it sound like that happens often. It's is at most once per game run. The JVM doesn't unload or gc code (unless you destroy a whole classloader context).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New (or improvement to existing) feature or request
Projects
None yet
Development

No branches or pull requests

5 participants