From 49f5b527dbcc0022e98ef66f83e6966673d9723f Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:03:01 -0500 Subject: [PATCH 01/52] Add JVM argument to help prevent mass Spark profile uploads --- .../modernfix/spark/SparkLaunchProfiler.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java b/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java index dacd6385..155fdae6 100644 --- a/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java +++ b/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java @@ -37,11 +37,26 @@ public class SparkLaunchProfiler { private static ExecutorService executor = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setDaemon(true).setNameFormat("spark-modernfix-async-worker").build()); private static final SparkPlatform platform = new SparkPlatform(new ModernFixSparkPlugin()); + private static final String ALLOW_SPARK_PROFILING_PROP = "modernfix.allowSparkProfiling"; private static final boolean USE_JAVA_SAMPLER_FOR_LAUNCH = !Boolean.getBoolean("modernfix.profileWithAsyncSampler"); + private static final boolean ALLOW_SPARK_PROFILING = Boolean.getBoolean(ALLOW_SPARK_PROFILING_PROP); private static final int SAMPLING_INTERVAL = Integer.getInteger("modernfix.profileSamplingIntervalMicroseconds", 4000); private static final String THREAD_GROUPER = System.getProperty("modernfix.profileSamplingThreadGrouper", "by-pool"); + private static boolean checkSparkProfilingAllowed() { + if (!ALLOW_SPARK_PROFILING) { + ModernFixMixinPlugin.instance.logger.fatal("To reduce excessive load on the Spark servers, you must set " + + "-D{}=true in your JVM arguments for profiling to proceed. Please do " + + "this and relaunch the game.", ALLOW_SPARK_PROFILING_PROP); + return false; + } + return true; + } + public static void start(String key) { + if (!checkSparkProfilingAllowed()) { + return; + } if (!ongoingSamplers.containsKey(key)) { Sampler sampler; SamplerSettings settings = new SamplerSettings(SAMPLING_INTERVAL, ThreadDumper.ALL, ThreadGrouper.parseConfigSetting(THREAD_GROUPER), -1, false); From 2ec6a6afbcf80a442fa8bcb8ae9872c58b521e94 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:39:49 -0500 Subject: [PATCH 02/52] Fix error running publishMods --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index f0dc1af8..26bd4c81 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -215,7 +215,7 @@ tasks.named("build") { } publishMods { - file.set(tasks.named(finalJarTask).get().outputs.files.singleFile) + file.set(tasks.named(finalJarTask).flatMap { it.archiveFile }) changelog = "Please check the [GitHub wiki](https://github.com/embeddedt/ModernFix/wiki/Changelog) for major changes." type = STABLE From a04266df54758ab486f0d7396ebbd7edb9dcfe86 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:47:26 -0500 Subject: [PATCH 03/52] Fix bugs in release process --- build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 26bd4c81..29f385ef 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -216,6 +216,7 @@ tasks.named("build") { publishMods { file.set(tasks.named(finalJarTask).flatMap { it.archiveFile }) + displayName.set(tasks.named(finalJarTask).flatMap { it.archiveFileName }) changelog = "Please check the [GitHub wiki](https://github.com/embeddedt/ModernFix/wiki/Changelog) for major changes." type = STABLE @@ -228,7 +229,7 @@ publishMods { minecraftVersions.add(minecraft_version) } modrinth { - projectId = "modernfix" + projectId = "nmDcB62a" accessToken = providers.environmentVariable("MODRINTH_TOKEN") minecraftVersions.add(minecraft_version) } From f26d35070e7ceb06671f2e7435900968668d88cb Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:01:26 -0500 Subject: [PATCH 04/52] Remove changelog step from release workflow [skip ci] --- .github/workflows/release.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc478189..90c0a0a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,10 +32,3 @@ jobs: with: files: 'bin/*' repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Add changelog to release - uses: irongut/EditRelease@v1.2.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - id: ${{ github.event.release.id }} - replacebody: true - files: "CHANGELOG.md" From 4dcdf09a0153968faa60a9e0c4bcda759ed3ba44 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:17:19 -0500 Subject: [PATCH 05/52] Do not convert ModFileScanData annotation values to immutable lists Related: #627 --- .../modernfix/forge/load/ModFileScanDataCompactor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java b/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java index 92ee07e7..7fb217e5 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java +++ b/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java @@ -8,7 +8,7 @@ import org.embeddedt.modernfix.ModernFix; import org.objectweb.asm.Type; import java.lang.reflect.Field; -import java.util.List; +import java.util.ArrayList; import java.util.Map; import java.util.stream.Collectors; @@ -65,8 +65,8 @@ public class ModFileScanDataCompactor { memberNames.addOrGet(a.memberName()), a.annotationData().entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> { Object annValue = e.getValue(); - if (annValue instanceof List list) { - annValue = List.copyOf(list); + if (annValue instanceof ArrayList list) { + list.trimToSize(); } return annValue; })) From a70f76a34dae74d033e486775bf4a2fec13219c3 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:20:48 -0500 Subject: [PATCH 06/52] Document the reason for lack of optimization --- .../modernfix/forge/load/ModFileScanDataCompactor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java b/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java index 7fb217e5..5bb82662 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java +++ b/src/main/java/org/embeddedt/modernfix/forge/load/ModFileScanDataCompactor.java @@ -66,6 +66,8 @@ public class ModFileScanDataCompactor { a.annotationData().entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> { Object annValue = e.getValue(); if (annValue instanceof ArrayList list) { + // We cannot properly compact annValue using List.of() because there are mods that + // (unnecessarily) rely on the list implementation being ArrayList. list.trimToSize(); } return annValue; From 9bc5f06a19bc9ea4c5b4109b0a5f97e6aeae43ac Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:39:05 -0500 Subject: [PATCH 07/52] Ensure correct order of properties in generated ModelResourceLocation variant strings Related: https://github.com/malte0811/FerriteCore/issues/219 --- .../forge/dynresources/ModelLocationBuilder.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/embeddedt/modernfix/forge/dynresources/ModelLocationBuilder.java b/src/main/java/org/embeddedt/modernfix/forge/dynresources/ModelLocationBuilder.java index 87d0098b..73e86966 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/dynresources/ModelLocationBuilder.java +++ b/src/main/java/org/embeddedt/modernfix/forge/dynresources/ModelLocationBuilder.java @@ -21,7 +21,17 @@ public class ModelLocationBuilder { private record PropertyData(ImmutableList nameValuePairs, int maxPairLength) {} public void generateForBlock(Set destinationSet, Block block, ResourceLocation baseLocation) { - var props = block.getStateDefinition().getProperties(); + // Make sure to iterate over the properties in the order of the getValues() map rather than using + // StateDefinition.getProperties(), to match the logic in BlockModelShaper.statePropertiesToString. + // In vanilla, these have the same order, because the backing implementation of getValues() is a map + // that preserves insertion order. However, in some versions of FerriteCore, getValues() may not + // preserve insertion order, but instead rely on hash order of the keys. This results in BlockModelShape + // and ModelLocationBuilder producing different MRLs. Using the keySet produces the same ordering as + // BlockModelShaper, provided that all states were built with the keys inserted in the same order into the same + // map implementation (which should always be true in practice). + // The above issue only seems to affect versions of FerriteCore after the switch to fastutil maps, but it + // is harmless to be consistent on older versions too, especially if another mod backports the fastutil change. + var props = block.defaultBlockState().getValues().keySet(); List> optionsList = new ArrayList<>(props.size()); int requiredBuilderSize = Math.max(0, props.size() - 1); // commas for (var prop : props) { From 7a8beea66e18fea53f309a659705cc955615a935 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 25 Jan 2026 12:35:07 -0500 Subject: [PATCH 08/52] Clear encoder cache when configuration finishes & on disconnect Credit to @XFactHD for the suggestion --- ...tConfigurationPacketListenerImplMixin.java | 21 ++++++++++++++++ .../encoder_cache_leak/MinecraftMixin.java | 25 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/ClientConfigurationPacketListenerImplMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/MinecraftMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/ClientConfigurationPacketListenerImplMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/ClientConfigurationPacketListenerImplMixin.java new file mode 100644 index 00000000..deb20085 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/ClientConfigurationPacketListenerImplMixin.java @@ -0,0 +1,21 @@ +package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak; + +import net.minecraft.client.multiplayer.ClientConfigurationPacketListenerImpl; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientConfigurationPacketListenerImpl.class) +@ClientOnlyMixin +public class ClientConfigurationPacketListenerImplMixin { + /** + * @author embeddedt + * @reason Reset the encoder cache after configuration finishes as the registries are now changing. + */ + @Inject(method = "handleConfigurationFinished", at = @At("RETURN")) + private void resetEncoderCache(CallbackInfo ci) { + ((EncoderCacheAccessor)DataComponentsAccessor.mfix$getCache()).mfix$getCache().invalidateAll(); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/MinecraftMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/MinecraftMixin.java new file mode 100644 index 00000000..91fe6326 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/MinecraftMixin.java @@ -0,0 +1,25 @@ +package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak; + +import net.minecraft.client.Minecraft; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) +@ClientOnlyMixin +public class MinecraftMixin { + /** + * @author embeddedt + * @reason Make sure the encoder cache is cleared when the client disconnects, as it retains strong references + * to registries. + */ + @Inject(method = { + "disconnect(Lnet/minecraft/client/gui/screens/Screen;Z)V", + "clearClientLevel(Lnet/minecraft/client/gui/screens/Screen;)V" + }, at = @At("RETURN")) + private void clearEncoderCache(CallbackInfo ci) { + ((EncoderCacheAccessor)DataComponentsAccessor.mfix$getCache()).mfix$getCache().invalidateAll(); + } +} From 334683fef684bd4ba6863a04d8329f11aecfa556 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:51:48 -0500 Subject: [PATCH 09/52] Remove obsolete Gradle file --- neoforge/gradle.properties | 1 - 1 file changed, 1 deletion(-) delete mode 100644 neoforge/gradle.properties diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties deleted file mode 100644 index 2914393d..00000000 --- a/neoforge/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -loom.platform=neoforge \ No newline at end of file From 3926f27d33ad00f8ed738c6297fa1b4652e3067c Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:27:27 -0500 Subject: [PATCH 10/52] Optimize memory usage of entity attribute templates --- .../AttributeSupplierBuilderMixin.java | 30 +++++++++++++ .../AttributeSupplierMixin.java | 32 ++++++++++++++ .../entity/AttributeInstanceTemplates.java | 44 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java new file mode 100644 index 00000000..2953a86a --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java @@ -0,0 +1,30 @@ +package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup; + +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import org.embeddedt.modernfix.entity.AttributeInstanceTemplates; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Map; + +@Mixin(AttributeSupplier.Builder.class) +public class AttributeSupplierBuilderMixin { + @Shadow + @Final + private Map builder; + + /** + * @author embeddedt + * @reason canonicalize identical AttributeInstance templates, many entities are created with the same values + */ + @Inject(method = "build", at = @At(value = "NEW", target = "(Ljava/util/Map;)Lnet/minecraft/world/entity/ai/attributes/AttributeSupplier;")) + private void deduplicateInstances(CallbackInfoReturnable cir) { + this.builder.replaceAll((a, i) -> AttributeInstanceTemplates.intern(i)); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java new file mode 100644 index 00000000..5ffc3489 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java @@ -0,0 +1,32 @@ +package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup; + +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; + +@Mixin(AttributeSupplier.class) +public class AttributeSupplierMixin { + @Shadow + @Final + @Mutable + private Map instances; + + /** + * @author embeddedt + * @reason Java 9's Map.of() implementation is significantly more compact than ImmutableMap, and we do not + * care about insertion order in this context + */ + @Inject(method = "", at = @At("RETURN")) + private void useCompactJavaMap(Map instances, CallbackInfo ci) { + this.instances = Map.copyOf(this.instances); + } +} \ No newline at end of file diff --git a/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java b/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java new file mode 100644 index 00000000..f54b2579 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java @@ -0,0 +1,44 @@ +package org.embeddedt.modernfix.entity; + +import it.unimi.dsi.fastutil.Hash; +import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; + +import java.util.Objects; + +public class AttributeInstanceTemplates { + private static final ObjectOpenCustomHashSet INTERNER = new ObjectOpenCustomHashSet<>(new Hash.Strategy<>() { + @Override + public int hashCode(AttributeInstance o) { + if (o == null) { + return 0; + } + int h = o.getAttribute().hashCode(); + h = 31 * h + Double.hashCode(o.getBaseValue()); + h = 31 * h + o.getModifiers().hashCode(); + return h; + } + + @Override + public boolean equals(AttributeInstance a, AttributeInstance b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return a.getAttribute() == b.getAttribute() + && a.getBaseValue() == b.getBaseValue() + && a.getModifiers().equals(b.getModifiers()); + } + }); + + public static AttributeInstance intern(AttributeInstance a) { + if (a == null || a.getClass() != AttributeInstance.class) { + return a; + } + synchronized (INTERNER) { + return INTERNER.addOrGet(a); + } + } +} From cff29149db251c827a63c6d346cadfaf182e3797 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:41:29 -0500 Subject: [PATCH 11/52] Intern map keys in BlockStateData --- .../perf/compact_mojang_registries/BlockStateDataMixin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_mojang_registries/BlockStateDataMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_mojang_registries/BlockStateDataMixin.java index eb85f08c..47b02f54 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_mojang_registries/BlockStateDataMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_mojang_registries/BlockStateDataMixin.java @@ -35,7 +35,7 @@ public class BlockStateDataMixin { t = compactTag(ct); } t = TAG_INTERNER.addOrGet(t); - entries[i++] = Map.entry(key, t); + entries[i++] = Map.entry(key.intern(), t); } return new CompoundTag(Map.ofEntries(entries)); } From d699187006cecd6a8294f0048aaa2041e6e8b967 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 25 Jan 2026 20:38:18 -0500 Subject: [PATCH 12/52] Fix AttachCapabilitiesEvent dispatch being very slow EventBus strikes again... --- .../AttachCapabilitiesEventMixin.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_cap_retrieval/AttachCapabilitiesEventMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_cap_retrieval/AttachCapabilitiesEventMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_cap_retrieval/AttachCapabilitiesEventMixin.java new file mode 100644 index 00000000..ee422cef --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_cap_retrieval/AttachCapabilitiesEventMixin.java @@ -0,0 +1,26 @@ +package org.embeddedt.modernfix.common.mixin.perf.forge_cap_retrieval; + +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.eventbus.api.Event; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(AttachCapabilitiesEvent.class) +public abstract class AttachCapabilitiesEventMixin extends Event { + /** + * @author embeddedt + * @reason EventSubclassTransformer is supposed to inject an override returning a constant on the class to avoid the + * {@link net.minecraftforge.eventbus.api.EventListenerHelper#isCancelable(Class)} slow path. + * However, the false case is only done for direct subclasses of Event (the true case is done for + * any cancelable event). This works for normal events because they must subclass Event directly, or be a subclass + * of an event that does. However, AttachCapabilitiesEvent subclasses GenericEvent, which does not pass through + * the EventSubclassTransformer as it comes from the EventBus library (where transformers are not run) rather than + * Forge which is on the GAME layer. The transformer on AttachCapabilitiesEvent then does not add the override as + * it expects it to be present on GenericEvent already. + *

+ * The simplest workaround to that whole mess is to just inject the override ourselves. + */ + @Override + public boolean isCancelable() { + return false; + } +} From 8125da7882c5dadd14bc421b6b8f2de21cba7ae8 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 25 Jan 2026 21:28:23 -0500 Subject: [PATCH 13/52] Avoid propagating unbaked model load errors to higher-level code Related: #625 --- .../perf/dynamic_resources/ModelManagerMixin.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java index 82e62917..c0a3916d 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java @@ -54,7 +54,8 @@ public class ModelManagerMixin { @ModifyArg(method = "loadBlockModels", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenCompose(Ljava/util/function/Function;)Ljava/util/concurrent/CompletableFuture;", ordinal = 0), index = 0) private static Function, ? extends CompletionStage>> deferBlockModelLoad(Function, ? extends CompletionStage>> fn, @Local(ordinal = 0, argsOnly = true) ResourceManager manager) { return resourceMap -> { - var cache = CacheUtil.simpleCacheForLambda(location -> loadSingleBlockModel(manager, location), 100L); + var fallbackModel = BlockModel.fromString(ModelBakery.MISSING_MODEL_MESH); + var cache = CacheUtil.simpleCacheForLambda(location -> loadSingleBlockModel(manager, location, fallbackModel), 100L); return CompletableFuture.completedFuture(Maps.asMap(Set.copyOf(resourceMap.keySet()), location -> cache.getUnchecked(location))); }; } @@ -70,13 +71,15 @@ public class ModelManagerMixin { return ImmutableList.of(); } - private static BlockModel loadSingleBlockModel(ResourceManager manager, ResourceLocation location) { + private static BlockModel loadSingleBlockModel(ResourceManager manager, ResourceLocation location, BlockModel fallbackModel) { return manager.getResource(location).map(resource -> { try (BufferedReader reader = resource.openAsReader()) { return BlockModel.fromStream(reader); - } catch(IOException e) { - ModernFix.LOGGER.error("Couldn't load model", e); - return null; + } catch (Exception e) { + // We must return some nonnull value to avoid breaking the map convention. The easiest solution + // is to just return a missing model template. + ModernFix.LOGGER.error("Couldn't load model {}, substituting missing", location, e); + return fallbackModel; } }).orElse(null); } From 5a93bc610973bf6631daf24dc077a629b2719726 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 25 Jan 2026 21:31:06 -0500 Subject: [PATCH 14/52] Use identityHashCode for attribute --- .../modernfix/entity/AttributeInstanceTemplates.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java b/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java index f54b2579..46e1328a 100644 --- a/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java +++ b/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java @@ -4,8 +4,6 @@ import it.unimi.dsi.fastutil.Hash; import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet; import net.minecraft.world.entity.ai.attributes.AttributeInstance; -import java.util.Objects; - public class AttributeInstanceTemplates { private static final ObjectOpenCustomHashSet INTERNER = new ObjectOpenCustomHashSet<>(new Hash.Strategy<>() { @Override @@ -13,7 +11,7 @@ public class AttributeInstanceTemplates { if (o == null) { return 0; } - int h = o.getAttribute().hashCode(); + int h = System.identityHashCode(o.getAttribute()); h = 31 * h + Double.hashCode(o.getBaseValue()); h = 31 * h + o.getModifiers().hashCode(); return h; From b9832b076b981e33b6beb40a91f151b258f0fa88 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 25 Jan 2026 21:31:18 -0500 Subject: [PATCH 15/52] Holder-ize AttributeSupplier mixins --- .../AttributeSupplierBuilderMixin.java | 3 ++- .../attribute_supplier_dedup/AttributeSupplierMixin.java | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java index 2953a86a..116e954d 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java @@ -1,5 +1,6 @@ package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup; +import net.minecraft.core.Holder; import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; @@ -17,7 +18,7 @@ import java.util.Map; public class AttributeSupplierBuilderMixin { @Shadow @Final - private Map builder; + private Map, AttributeInstance> builder; /** * @author embeddedt diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java index 5ffc3489..c214cba0 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java @@ -1,5 +1,6 @@ package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup; +import net.minecraft.core.Holder; import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; @@ -18,7 +19,7 @@ public class AttributeSupplierMixin { @Shadow @Final @Mutable - private Map instances; + private Map, AttributeInstance> instances; /** * @author embeddedt @@ -26,7 +27,7 @@ public class AttributeSupplierMixin { * care about insertion order in this context */ @Inject(method = "", at = @At("RETURN")) - private void useCompactJavaMap(Map instances, CallbackInfo ci) { + private void useCompactJavaMap(Map, AttributeInstance> instances, CallbackInfo ci) { this.instances = Map.copyOf(this.instances); } } \ No newline at end of file From 8c34c0de50f9241f3e630a69bfee9ad44b229dc7 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:50:21 -0500 Subject: [PATCH 16/52] Dump stats on permanently loaded baked models to debug log --- .../ForgeHooksClientMixin.java | 4 ++ .../DynamicBakedModelProvider.java | 45 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ForgeHooksClientMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ForgeHooksClientMixin.java index 09398c75..31879cff 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ForgeHooksClientMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ForgeHooksClientMixin.java @@ -12,6 +12,7 @@ import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.ModLoader; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.dynamicresources.DynamicBakedModelProvider; import org.embeddedt.modernfix.forge.dynresources.ModelBakeEventHelper; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -62,5 +63,8 @@ public class ForgeHooksClientMixin { ModernFix.LOGGER.warn(" {}: {}", entry.getKey(), entry.getValue().toString()); }); } + if (bakeEvent.getModels() instanceof DynamicBakedModelProvider dynamicProvider) { + dynamicProvider.dumpStats(); + } } } diff --git a/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicBakedModelProvider.java b/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicBakedModelProvider.java index 288b4028..fec582d4 100644 --- a/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicBakedModelProvider.java +++ b/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicBakedModelProvider.java @@ -2,6 +2,8 @@ package org.embeddedt.modernfix.dynamicresources; import com.google.common.collect.ImmutableSet; import com.mojang.math.Transformation; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.client.renderer.block.model.BakedQuad; import net.minecraft.client.renderer.block.model.ItemOverrides; @@ -248,4 +250,47 @@ public class DynamicBakedModelProvider implements Map, Object2IntOpenHashMap> byClassAndNamespace = new Object2ObjectOpenHashMap<>(); + Object2IntOpenHashMap> totalsByClass = new Object2IntOpenHashMap<>(); + synchronized (permanentOverrides) { + for (var entry : permanentOverrides.entrySet()) { + var model = entry.getValue(); + if (model == null) { + continue; + } + totalsByClass.addTo(model.getClass(), 1); + var byNamespace = byClassAndNamespace.computeIfAbsent(model.getClass(), $ -> new Object2IntOpenHashMap<>()); + byNamespace.addTo(entry.getKey().getNamespace(), 1); + } + } + ModernFix.LOGGER.debug("Loaded {} permanent overrides", permanentOverrides.size()); + byClassAndNamespace.entrySet().stream().sorted((a, b) -> + Integer.compare( + totalsByClass.getInt(b.getKey()), + totalsByClass.getInt(a.getKey()) + )) + .forEach(classEntry -> { + var byNamespace = classEntry.getValue(); + int totalModels = totalsByClass.getInt(classEntry.getKey()); + ModernFix.LOGGER.debug( + "{}: {} models", + classEntry.getKey().getName(), + totalModels + ); + + // sort namespaces by count (descending) + byNamespace.object2IntEntrySet().stream() + .sorted((a, b) -> + Integer.compare(b.getIntValue(), a.getIntValue())) + .forEach(nsEntry -> { + ModernFix.LOGGER.debug( + " {}: {}", + nsEntry.getKey(), + nsEntry.getIntValue() + ); + }); + }); + } } From bc0e9a09fce1e132388c7480a3c861d487f57cd9 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:29:14 -0500 Subject: [PATCH 17/52] Prevent model locations added in RegisterAdditional from being early baked --- .../dynamic_resources/ModelBakeryMixin.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java index c7719e3d..f8a08e42 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelBakeryMixin.java @@ -7,6 +7,8 @@ import com.google.common.cache.RemovalNotification; import com.google.common.collect.ForwardingMap; import com.google.common.collect.ImmutableList; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import net.minecraft.client.Minecraft; import net.minecraft.client.color.block.BlockColors; import net.minecraft.client.renderer.block.model.BlockModel; @@ -187,6 +189,20 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { return ImmutableList.of(); } + /** + * @author embeddedt + * @reason Prevent the models provided by RegisterAdditional from being tracked, otherwise the unbaked models will + * be loaded, baked, and added to permanent overrides unnecessarily. + * We still need to fire the event, because there is a decent chance mods rely on it to set up state for their + * model loaders, but we can ignore the return value. + */ + @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/client/ForgeHooksClient;onRegisterAdditionalModels(Ljava/util/Set;)V")) + private void preventLoadOfAdditionalModels(Set additionalModels, Operation original) { + original.call(additionalModels); + // Immediately clear the set + additionalModels.clear(); + } + /** * Make a copy of the top-level model list to avoid CME if more models get loaded here. */ @@ -256,7 +272,9 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { cir.setReturnValue(existing); } else { synchronized(this) { - if (this.loadingStack.contains(modelLocation)) { + // CIT Resewn adds dependencies to loadingStack outside of getModel(), so we must silently + // ignore existing things in the stack for the outermost model + if (mfix$nestedLoads > 0 && this.loadingStack.contains(modelLocation)) { throw new IllegalStateException("Circular reference while loading " + modelLocation); } else { this.loadingStack.add(modelLocation); From 878b3798f312c5afbe8ee5077721392f30672b2c Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:10:06 -0500 Subject: [PATCH 18/52] Detect mods causing CMEs with the client resource reload listener list Related: #512 --- .../ReloadableResourceManagerMixin.java | 49 +++++++++++++++++++ .../common/mixin/core/GameDataMixin.java | 16 ++++++ .../modernfix/forge/init/ModernFixForge.java | 1 + 3 files changed, 66 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/concurrency/ReloadableResourceManagerMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/core/GameDataMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/concurrency/ReloadableResourceManagerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/concurrency/ReloadableResourceManagerMixin.java new file mode 100644 index 00000000..e056c64f --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/concurrency/ReloadableResourceManagerMixin.java @@ -0,0 +1,49 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.concurrency; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.client.Minecraft; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ReloadableResourceManager; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.ModLoadingStage; +import net.minecraftforge.registries.ForgeRegistries; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.forge.init.ModernFixForge; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ReloadableResourceManager.class) +@ClientOnlyMixin +public abstract class ReloadableResourceManagerMixin { + @Shadow + @Final + private PackType type; + + @Shadow + public abstract void registerReloadListener(PreparableReloadListener listener); + + /** + * @author embeddedt + * @reason complain loudly when reload listeners are being registered too late in a way that would cause + * concurrency issues, and prevent them from crashing the game + */ + @WrapMethod(method = "registerReloadListener") + private void checkCallingThread(PreparableReloadListener listener, Operation original) { + if (ModernFixForge.registryEventsFired && this.type == PackType.CLIENT_RESOURCES + && (Object)this == Minecraft.getInstance().getResourceManager() + && !Minecraft.getInstance().isSameThread()) { + ModernFix.LOGGER.error("A mod is calling registerReloadListener at the wrong time. This will cause random concurrency crashes when ModernFix is not installed. Please report this to them. If you are a modder, refer to https://github.com/embeddedt/ModernFix/wiki/registerReloadListener-called-on-wrong-thread for more information.", new Exception("registerReloadListener called on wrong thread")); + // Defer the call onto the main client thread. There is a decent chance the mod's listener will be + // ignored in this case, but it is more predictable than allowing them to randomly crash the game. + Minecraft.getInstance().tell(() -> this.registerReloadListener(listener)); + return; + } + + original.call(listener); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/core/GameDataMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/core/GameDataMixin.java new file mode 100644 index 00000000..3756e72b --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/core/GameDataMixin.java @@ -0,0 +1,16 @@ +package org.embeddedt.modernfix.common.mixin.core; + +import net.minecraftforge.registries.GameData; +import org.embeddedt.modernfix.forge.init.ModernFixForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = GameData.class, remap = false) +public class GameDataMixin { + @Inject(method = "postRegisterEvents", at = @At("RETURN")) + private static void markPosted(CallbackInfo ci) { + ModernFixForge.registryEventsFired = true; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java index dd2cd228..a95cbdd2 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java +++ b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java @@ -38,6 +38,7 @@ import java.util.List; public class ModernFixForge { private static ModernFix commonMod; public static boolean launchDone = false; + public static boolean registryEventsFired = false; public ModernFixForge() { commonMod = new ModernFix(); From b9933b1158cafa7eac22a2d5090c3277a2686503 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:42:20 -0500 Subject: [PATCH 19/52] Add bytecode analysis to filter ICapabilityProvider impls where possible Currently disabled by default till more testing is completed --- .../core/config/ModernFixEarlyConfig.java | 1 + ...CapabilityProviderDispatcherGenerator.java | 134 ++++-- .../analysis/CapabilityAnalysisResult.java | 23 + .../analysis/CapabilityAnalyzer.java | 392 ++++++++++++++++++ .../capability/analysis/CapabilityRef.java | 14 + .../analysis/CapabilitySourceInterpreter.java | 49 +++ 6 files changed, 576 insertions(+), 37 deletions(-) create mode 100644 src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalysisResult.java create mode 100644 src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java create mode 100644 src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityRef.java create mode 100644 src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilitySourceInterpreter.java diff --git a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index 99b12a07..094cfa99 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -174,6 +174,7 @@ public class ModernFixEarlyConfig { .put("mixin.feature.blockentity_incorrect_thread", false) .put("mixin.perf.clear_mixin_classinfo", false) .put("mixin.perf.deduplicate_climate_parameters", false) + .put("mixin.perf.faster_capabilities.bytecode_analysis", false) .put("mixin.bugfix.packet_leak", false) .put("mixin.perf.deduplicate_location", false) .put("mixin.perf.dynamic_entity_renderers", false) diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java index 8a70b589..05dc3c4d 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -2,13 +2,21 @@ package org.embeddedt.modernfix.forge.capability; import net.minecraftforge.common.capabilities.ICapabilityProvider; import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalysisResult; +import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalyzer; +import org.embeddedt.modernfix.forge.capability.analysis.CapabilityRef; import org.objectweb.asm.*; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; +import java.lang.reflect.Modifier; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -23,6 +31,7 @@ import static org.objectweb.asm.Opcodes.*; * and performs direct dispatch instead of megamorphic virtual calls. */ public class CapabilityProviderDispatcherGenerator { + private static final String GENERATED_CLASSES_FOLDER = System.getProperty("modernfix.generatedCapabilityDispatcherClassDumpFolder", ""); private static final ConcurrentHashMap>, MethodHandle> cache = new ConcurrentHashMap<>(); @@ -67,10 +76,27 @@ public class CapabilityProviderDispatcherGenerator { } private static MethodHandle generateClass(List> providerTypes) { - ModernFix.LOGGER.debug("Generating capability dispatcher for types: [{}]", providerTypes.stream().map(Class::getName).collect(Collectors.joining(", "))); try { - String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + classCounter.incrementAndGet(); - byte[] classBytes = generateClassBytes(className, providerTypes); + // Analyze each provider type + List analysisResults = new ArrayList<>(providerTypes.size()); + for (Class type : providerTypes) { + CapabilityAnalysisResult result = CapabilityAnalyzer.analyze(type); + analysisResults.add(result); + } + + int generatedClassId = classCounter.incrementAndGet(); + String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + generatedClassId; + + ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < providerTypes.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(providerTypes.get(i).getName()).append(" -> ").append(formatAnalysisResult(analysisResults.get(i))); + } + return sb; + }); + + byte[] classBytes = generateClassBytes(className, providerTypes, analysisResults); // Define the hidden class MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass( @@ -79,6 +105,12 @@ public class CapabilityProviderDispatcherGenerator { MethodHandles.Lookup.ClassOption.NESTMATE ); + if (!GENERATED_CLASSES_FOLDER.isBlank()) { + Path path = Paths.get(GENERATED_CLASSES_FOLDER, "generatedDispatcher" + generatedClassId + ".class"); + Files.createDirectories(path.getParent()); + Files.write(path, classBytes); + } + // Return a MethodHandle to the constructor // Constructor signature: (ICapabilityProvider[])V // The constructor is adapted to take an Object and return an ICapabilityProvider to match @@ -92,7 +124,7 @@ public class CapabilityProviderDispatcherGenerator { } } - private static byte[] generateClassBytes(String className, List> providerTypes) { + private static byte[] generateClassBytes(String className, List> providerTypes, List analysisResults) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); // Class declaration: implements ICapabilityProvider @@ -105,28 +137,36 @@ public class CapabilityProviderDispatcherGenerator { new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" } ); + // Compute field descriptors: use concrete type when possible for JIT devirtualization + String[] fieldDescs = new String[providerTypes.size()]; + for (int i = 0; i < providerTypes.size(); i++) { + Class type = providerTypes.get(i); + fieldDescs[i] = (!type.isHidden() && Modifier.isPublic(type.getModifiers())) + ? Type.getDescriptor(type) : ICAP_PROVIDER_DESC; + } + // Generate final fields for each provider for (int i = 0; i < providerTypes.size(); i++) { cw.visitField( ACC_PRIVATE | ACC_FINAL, "provider" + i, - ICAP_PROVIDER_DESC, + fieldDescs[i], null, null ).visitEnd(); } // Generate constructor - generateConstructor(cw, className, providerTypes.size()); + generateConstructor(cw, className, providerTypes.size(), fieldDescs); // Generate getCapability method with sided parameter - generateGetCapabilityMethod(cw, className, providerTypes.size()); + generateGetCapabilityMethod(cw, className, fieldDescs, analysisResults); cw.visitEnd(); return cw.toByteArray(); } - private static void generateConstructor(ClassWriter cw, String className, int providerCount) { + private static void generateConstructor(ClassWriter cw, String className, int providerCount, String[] fieldDescs) { Method constructor = Method.getMethod("void (net.minecraftforge.common.capabilities.ICapabilityProvider[])"); GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw); @@ -136,14 +176,18 @@ public class CapabilityProviderDispatcherGenerator { // Unpack array into final fields for (int i = 0; i < providerCount; i++) { + Type fieldType = Type.getType(fieldDescs[i]); mg.loadThis(); // this mg.loadArg(0); // array mg.push(i); // index mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); // array[i] + if (!fieldDescs[i].equals(ICAP_PROVIDER_DESC)) { + mg.checkCast(fieldType); + } mg.putField( Type.getObjectType(className.replace('.', '/')), "provider" + i, - Type.getType(ICAP_PROVIDER_DESC) + fieldType ); } @@ -151,7 +195,9 @@ public class CapabilityProviderDispatcherGenerator { mg.endMethod(); } - private static void generateGetCapabilityMethod(ClassWriter cw, String className, int providerCount) { + private static void generateGetCapabilityMethod(ClassWriter cw, String className, String[] fieldDescs, List analysisResults) { + int providerCount = fieldDescs.length; + // Method: LazyOptional getCapability(Capability, Direction) MethodVisitor mv = cw.visitMethod( ACC_PUBLIC, @@ -168,15 +214,45 @@ public class CapabilityProviderDispatcherGenerator { Label endLabel = new Label(); for (int i = 0; i < providerCount; i++) { + CapabilityAnalysisResult analysis = analysisResults.get(i); Label nextLabel = new Label(); + // AlwaysEmpty: skip code generation for this provider entirely + if (analysis instanceof CapabilityAnalysisResult.AlwaysEmpty) { + continue; + } + + // KnownCapabilities: emit guard checks before dispatch + if (analysis instanceof CapabilityAnalysisResult.KnownCapabilities known + && known.capabilities().size() <= 5) { + if (known.capabilities().size() == 1) { + // Single cap: if (cap != KNOWN_CAP) goto nextProvider + CapabilityRef ref = known.capabilities().iterator().next(); + mv.visitVarInsn(ALOAD, 1); // cap parameter + mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); + mv.visitJumpInsn(IF_ACMPNE, nextLabel); + } else { + // Multiple caps: check each, jump to callProvider on match + Label callProvider = new Label(); + for (CapabilityRef ref : known.capabilities()) { + mv.visitVarInsn(ALOAD, 1); // cap parameter + mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); + mv.visitJumpInsn(IF_ACMPEQ, callProvider); + } + // No match, skip this provider + mv.visitJumpInsn(GOTO, nextLabel); + mv.visitLabel(callProvider); + } + } + // Indeterminate: no guard, fall through to dispatch + // LazyOptional result = this.providerN.getCapability(cap, side); mv.visitVarInsn(ALOAD, 0); // this mv.visitFieldInsn( GETFIELD, className.replace('.', '/'), "provider" + i, - ICAP_PROVIDER_DESC + fieldDescs[i] ); mv.visitVarInsn(ALOAD, 1); // cap parameter mv.visitVarInsn(ALOAD, 2); // side parameter @@ -211,9 +287,6 @@ public class CapabilityProviderDispatcherGenerator { mv.visitInsn(ARETURN); mv.visitLabel(nextLabel); - if (i < providerCount - 1) { - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - } } // If no provider returned a capability, return empty @@ -231,29 +304,16 @@ public class CapabilityProviderDispatcherGenerator { mv.visitEnd(); } - /** - * Creates an instance of the optimized dispatcher for the given providers. - */ - public static ICapabilityProvider createDispatcher(ICapabilityProvider[] providers) { - try { - MethodHandle constructor = getOrGenerateConstructor(providers); - return (ICapabilityProvider) constructor.invoke(providers); - } catch (Throwable e) { - throw new RuntimeException("Failed to create capability dispatcher instance", e); + private static String formatAnalysisResult(CapabilityAnalysisResult result) { + if (result instanceof CapabilityAnalysisResult.AlwaysEmpty) { + return "always empty (skipped)"; + } else if (result instanceof CapabilityAnalysisResult.KnownCapabilities known) { + return "known caps: " + known.capabilities().stream() + .map(ref -> ref.owner() + "#" + ref.fieldName()) + .collect(Collectors.joining(", ")); + } else if (result instanceof CapabilityAnalysisResult.Indeterminate ind) { + return "indeterminate (" + ind.reason() + ")"; } - } - - /** - * Clears the cache of generated classes. Use with caution. - */ - public static void clearCache() { - cache.clear(); - } - - /** - * Returns the number of cached dispatcher classes. - */ - public static int getCacheSize() { - return cache.size(); + return result.toString(); } } \ No newline at end of file diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalysisResult.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalysisResult.java new file mode 100644 index 00000000..4795a410 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalysisResult.java @@ -0,0 +1,23 @@ +package org.embeddedt.modernfix.forge.capability.analysis; + +import java.util.Set; + +/** + * Result of analyzing a capability provider's {@code getCapability} bytecode. + */ +public sealed interface CapabilityAnalysisResult { + /** + * The provider can only return non-empty for these specific capabilities. + */ + record KnownCapabilities(Set capabilities) implements CapabilityAnalysisResult {} + + /** + * The provider always returns {@code LazyOptional.empty()}. + */ + record AlwaysEmpty() implements CapabilityAnalysisResult {} + + /** + * Analysis could not determine the set of capabilities; fall back to unguarded dispatch. + */ + record Indeterminate(String reason) implements CapabilityAnalysisResult {} +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java new file mode 100644 index 00000000..bcd8e1ae --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java @@ -0,0 +1,392 @@ +package org.embeddedt.modernfix.forge.capability.analysis; + +import net.minecraft.core.Direction; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.core.ModernFixMixinPlugin; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SourceValue; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Analyzes {@code getCapability} bytecode to determine which capabilities a provider handles. + */ +public class CapabilityAnalyzer { + + private static final ConcurrentHashMap, CapabilityAnalysisResult> cache = new ConcurrentHashMap<>(); + + private static final String CAPABILITY_INTERNAL = "net/minecraftforge/common/capabilities/Capability"; + private static final String CAPABILITY_DESC = "Lnet/minecraftforge/common/capabilities/Capability;"; + private static final String LAZY_OPTIONAL_INTERNAL = "net/minecraftforge/common/util/LazyOptional"; + private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;"; + private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;"; + private static final String GET_CAPABILITY_DESC = "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC; + private static final String ICAP_PROVIDER_INTERNAL = "net/minecraftforge/common/capabilities/ICapabilityProvider"; + + public static CapabilityAnalysisResult analyze(Class clazz) { + if (!ModernFixMixinPlugin.instance.isOptionEnabled("perf.faster_capabilities.bytecode_analysis.CapabilityAnalyzer")) { + return new CapabilityAnalysisResult.Indeterminate("bytecode analysis disabled"); + } + CapabilityAnalysisResult result = cache.get(clazz); + if (result != null) return result; + result = doAnalyzeSafe(clazz); + CapabilityAnalysisResult existing = cache.putIfAbsent(clazz, result); + return existing != null ? existing : result; + } + + private static CapabilityAnalysisResult doAnalyzeSafe(Class clazz) { + try { + return doAnalyze(clazz); + } catch (Exception e) { + ModernFix.LOGGER.debug("Capability analysis failed for {}: {}", clazz.getName(), e.getMessage()); + return new CapabilityAnalysisResult.Indeterminate("analysis exception: " + e.getMessage()); + } + } + + private static CapabilityAnalysisResult doAnalyze(Class clazz) throws IOException, AnalyzerException { + // Find the class that actually declares getCapability via reflection + Class declaringClass; + try { + declaringClass = clazz.getMethod("getCapability", Capability.class, Direction.class).getDeclaringClass(); + } catch (NoSuchMethodException e) { + return new CapabilityAnalysisResult.AlwaysEmpty(); + } + + if (declaringClass.getName().replace('.', '/').equals(ICAP_PROVIDER_INTERNAL)) { + return new CapabilityAnalysisResult.AlwaysEmpty(); + } + + String declaringClassName = declaringClass.getName().replace('.', '/'); + ClassNode declaringClassNode = readClass(declaringClass); + if (declaringClassNode == null) { + return new CapabilityAnalysisResult.Indeterminate("cannot read bytecode for " + declaringClass.getName()); + } + + MethodNode getCapMethod = findGetCapabilityMethod(declaringClassNode); + if (getCapMethod == null) { + return new CapabilityAnalysisResult.Indeterminate("method not found in bytecode for " + declaringClass.getName()); + } + + // Run the source analysis + CapabilitySourceInterpreter interpreter = new CapabilitySourceInterpreter(); + Analyzer analyzer = new Analyzer<>(interpreter); + Frame[] frames = analyzer.analyze(declaringClassName, getCapMethod); + + // Build if-guard map: maps instruction indices to CapabilityRef for guarded regions + List guardRegions = findGuardRegions(getCapMethod, frames); + + // Classify each ARETURN + InsnList instructions = getCapMethod.instructions; + Set knownCaps = new HashSet<>(); + boolean hasIndeterminate = false; + String indeterminateReason = null; + + for (int i = 0; i < instructions.size(); i++) { + AbstractInsnNode insn = instructions.get(i); + if (insn.getOpcode() != Opcodes.ARETURN) continue; + + Frame frame = frames[i]; + if (frame == null) continue; // dead code + + SourceValue topOfStack = frame.getStack(frame.getStackSize() - 1); + + ReturnClassification classification = classifyReturnSources( + topOfStack, interpreter, i, guardRegions, clazz, instructions); + + if (classification instanceof ReturnClassification.Known known) { + knownCaps.addAll(known.caps); + } else if (classification instanceof ReturnClassification.Unknown unknown) { + hasIndeterminate = true; + indeterminateReason = unknown.reason; + } + // Empty: skip + } + + if (hasIndeterminate) { + CapabilityAnalysisResult result = new CapabilityAnalysisResult.Indeterminate(indeterminateReason); + ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result); + return result; + } + + if (knownCaps.isEmpty()) { + ModernFix.LOGGER.debug("Capability analysis for {}: AlwaysEmpty", clazz.getName()); + return new CapabilityAnalysisResult.AlwaysEmpty(); + } + + CapabilityAnalysisResult result = new CapabilityAnalysisResult.KnownCapabilities(Set.copyOf(knownCaps)); + ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result); + return result; + } + + private static ReturnClassification classifyReturnSources( + SourceValue topOfStack, + CapabilitySourceInterpreter interpreter, + int returnIndex, + List guardRegions, + Class originalClass, + InsnList instructions) { + + Set caps = new HashSet<>(); + List unknownSources = new ArrayList<>(); + String unknownReason = null; + + for (AbstractInsnNode source : topOfStack.insns) { + ReturnClassification sourceClassification = classifySingleSource( + source, interpreter, originalClass); + + if (sourceClassification instanceof ReturnClassification.Unknown unknown) { + unknownSources.add(source); + unknownReason = unknown.reason; + } else if (sourceClassification instanceof ReturnClassification.Known known) { + caps.addAll(known.caps); + } + } + + // If any source is unknown, try the guard region fallback before giving up + if (!unknownSources.isEmpty()) { + // Check if the return itself is in a guard region + for (GuardRegion guard : guardRegions) { + if (returnIndex > guard.guardIndex && returnIndex < guard.targetIndex) { + return new ReturnClassification.Known(Set.of(guard.capabilityRef)); + } + } + // Also check if each unknown source instruction is inside a guard region. + // This handles ternary patterns where both branches merge into a single + // ARETURN after the guard, but the unknown value was produced inside it. + boolean allResolved = true; + for (AbstractInsnNode unknownSource : unknownSources) { + int sourceIndex = instructions.indexOf(unknownSource); + boolean resolved = false; + for (GuardRegion guard : guardRegions) { + if (sourceIndex > guard.guardIndex && sourceIndex < guard.targetIndex) { + caps.add(guard.capabilityRef); + resolved = true; + break; + } + } + if (!resolved) { + allResolved = false; + break; + } + } + if (!allResolved) { + return new ReturnClassification.Unknown(unknownReason); + } + } + + if (caps.isEmpty()) { + return ReturnClassification.EMPTY; + } + return new ReturnClassification.Known(caps); + } + + private static ReturnClassification classifySingleSource( + AbstractInsnNode source, + CapabilitySourceInterpreter interpreter, + Class originalClass) { + + if (source instanceof MethodInsnNode methodInsn) { + // Case: Capability.orEmpty(...) + if (methodInsn.getOpcode() == Opcodes.INVOKEVIRTUAL + && methodInsn.owner.equals(CAPABILITY_INTERNAL) + && methodInsn.name.equals("orEmpty")) { + return classifyOrEmptyCall(methodInsn, interpreter); + } + + // Case: LazyOptional.empty() + if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC + && methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL) + && methodInsn.name.equals("empty")) { + return ReturnClassification.EMPTY; + } + + // Case: LazyOptional.of(null) + if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC + && methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL) + && methodInsn.name.equals("of")) { + List args = interpreter.getCallArguments(methodInsn); + if (!args.isEmpty()) { + SourceValue arg = args.get(0); + if (arg.insns.size() == 1 + && arg.insns.iterator().next().getOpcode() == Opcodes.ACONST_NULL) { + return ReturnClassification.EMPTY; + } + } + } + + // Case: super.getCapability(...) + if (methodInsn.getOpcode() == Opcodes.INVOKESPECIAL + && methodInsn.name.equals("getCapability") + && methodInsn.desc.equals(GET_CAPABILITY_DESC)) { + return classifySuperDelegation(originalClass); + } + } + + return new ReturnClassification.Unknown("unclassified source: " + source.getClass().getSimpleName() + + " opcode=" + source.getOpcode()); + } + + private static ReturnClassification classifyOrEmptyCall( + MethodInsnNode methodInsn, CapabilitySourceInterpreter interpreter) { + List args = interpreter.getCallArguments(methodInsn); + if (args.isEmpty()) { + return new ReturnClassification.Unknown("orEmpty call with no recorded arguments"); + } + + // arg 0 is the receiver (the Capability instance) + SourceValue receiver = args.get(0); + for (AbstractInsnNode recvSource : receiver.insns) { + if (recvSource instanceof FieldInsnNode fieldInsn + && fieldInsn.getOpcode() == Opcodes.GETSTATIC + && fieldInsn.desc.equals(CAPABILITY_DESC)) { + return new ReturnClassification.Known( + Set.of(new CapabilityRef(fieldInsn.owner, fieldInsn.name))); + } + } + + return new ReturnClassification.Unknown("orEmpty receiver is not a static Capability field"); + } + + private static ReturnClassification classifySuperDelegation(Class originalClass) { + Class superClass = originalClass.getSuperclass(); + if (superClass == null || superClass == Object.class) { + return ReturnClassification.EMPTY; + } + + @SuppressWarnings("unchecked") + Class superProvider = + (Class) superClass; + CapabilityAnalysisResult superResult = analyze(superProvider); + if (superResult instanceof CapabilityAnalysisResult.KnownCapabilities known) { + return new ReturnClassification.Known(known.capabilities()); + } else if (superResult instanceof CapabilityAnalysisResult.AlwaysEmpty) { + return ReturnClassification.EMPTY; + } else if (superResult instanceof CapabilityAnalysisResult.Indeterminate ind) { + return new ReturnClassification.Unknown("super delegation: " + ind.reason()); + } + return ReturnClassification.EMPTY; + } + + private static List findGuardRegions(MethodNode method, Frame[] frames) { + List regions = new ArrayList<>(); + InsnList instructions = method.instructions; + + for (int i = 0; i < instructions.size(); i++) { + AbstractInsnNode insn = instructions.get(i); + int opcode = insn.getOpcode(); + if (opcode != Opcodes.IF_ACMPEQ && opcode != Opcodes.IF_ACMPNE) continue; + + Frame frame = frames[i]; + if (frame == null || frame.getStackSize() < 2) continue; + + SourceValue val1 = frame.getStack(frame.getStackSize() - 2); + SourceValue val2 = frame.getStack(frame.getStackSize() - 1); + + // Check if one traces to ALOAD 1 (cap parameter) and other to GETSTATIC Capability + CapabilityRef capRef = findCapRef(val1); + if (capRef == null) capRef = findCapRef(val2); + boolean hasParamLoad = isCapParamLoad(val1) || isCapParamLoad(val2); + + if (capRef != null && hasParamLoad) { + JumpInsnNode jumpInsn = (JumpInsnNode) insn; + int targetIndex = instructions.indexOf(jumpInsn.label); + + if (opcode == Opcodes.IF_ACMPNE) { + // if (cap != KNOWN_CAP) goto target -> the code between insn and target is for KNOWN_CAP + regions.add(new GuardRegion(capRef, i, targetIndex)); + } else { + // IF_ACMPEQ: if (cap == KNOWN_CAP) goto target -> the target is the guarded code + // Find the end of the guarded region (next label or return) + // For simplicity, mark from target to the next unconditional branch or return + // TODO: that simplification may have edge cases + int endIndex = findGuardedRegionEnd(instructions, targetIndex); + regions.add(new GuardRegion(capRef, targetIndex, endIndex)); + } + } + } + + return regions; + } + + private static int findGuardedRegionEnd(InsnList instructions, int startIndex) { + for (int i = startIndex; i < instructions.size(); i++) { + AbstractInsnNode insn = instructions.get(i); + int opcode = insn.getOpcode(); + if (opcode == Opcodes.GOTO || opcode == Opcodes.ARETURN + || opcode == Opcodes.RETURN || opcode == Opcodes.ATHROW) { + return i + 1; + } + } + return instructions.size(); + } + + private static CapabilityRef findCapRef(SourceValue sv) { + CapabilityRef ref = null; + for (AbstractInsnNode src : sv.insns) { + if (src instanceof FieldInsnNode fieldInsn + && fieldInsn.getOpcode() == Opcodes.GETSTATIC + && fieldInsn.desc.equals(CAPABILITY_DESC)) { + if (ref == null) { + ref = new CapabilityRef(fieldInsn.owner, fieldInsn.name); + } else if (!ref.owner().equals(fieldInsn.owner) || !ref.fieldName().equals(fieldInsn.name)) { + return null; // ambiguous: multiple different capability fields + } + } else { + return null; // non-capability source + } + } + return ref; + } + + private static boolean isCapParamLoad(SourceValue sv) { + if (sv.insns.isEmpty()) return false; + for (AbstractInsnNode src : sv.insns) { + if (!(src instanceof VarInsnNode varInsn) + || varInsn.getOpcode() != Opcodes.ALOAD + || varInsn.var != 1) { + return false; + } + } + return true; + } + + private static MethodNode findGetCapabilityMethod(ClassNode classNode) { + for (MethodNode method : classNode.methods) { + if (method.name.equals("getCapability") && method.desc.equals(GET_CAPABILITY_DESC)) { + return method; + } + } + return null; + } + + private static ClassNode readClass(Class clazz) throws IOException { + String resourcePath = "/" + clazz.getName().replace('.', '/') + ".class"; + try (InputStream is = clazz.getResourceAsStream(resourcePath)) { + if (is == null) return null; + ClassReader reader = new ClassReader(is); + ClassNode node = new ClassNode(); + reader.accept(node, 0); + return node; + } + } + + private sealed interface ReturnClassification { + ReturnClassification EMPTY = new Empty(); + + record Known(Set caps) implements ReturnClassification {} + record Empty() implements ReturnClassification {} + record Unknown(String reason) implements ReturnClassification {} + } + + private record GuardRegion(CapabilityRef capabilityRef, int guardIndex, int targetIndex) {} +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityRef.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityRef.java new file mode 100644 index 00000000..d6da747d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityRef.java @@ -0,0 +1,14 @@ +package org.embeddedt.modernfix.forge.capability.analysis; + +/** + * Identifies a capability by the static field that holds it. + * + * @param owner internal class name (e.g. {@code net/minecraftforge/common/capabilities/ForgeCapabilities}) + * @param fieldName field name (e.g. {@code ITEM_HANDLER}) + */ +public record CapabilityRef(String owner, String fieldName) { + @Override + public String toString() { + return owner + "." + fieldName; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilitySourceInterpreter.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilitySourceInterpreter.java new file mode 100644 index 00000000..a2df3d2e --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilitySourceInterpreter.java @@ -0,0 +1,49 @@ +package org.embeddedt.modernfix.forge.capability.analysis; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; + +import java.util.*; + +/** + * Extends {@link SourceInterpreter} with two enhancements: + *

    + *
  • Propagates source values through copy operations (ALOAD/ASTORE), so that + * {@code result = cap.orEmpty(...); return result;} traces back to the INVOKEVIRTUAL.
  • + *
  • Records argument {@link SourceValue}s for each call instruction, so we can later + * inspect the receiver/arguments of {@code orEmpty()} calls.
  • + *
+ */ +public class CapabilitySourceInterpreter extends SourceInterpreter { + + private final Map> callArguments = new HashMap<>(); + + public CapabilitySourceInterpreter() { + super(ASM9); + } + + /** + * Returns the recorded argument SourceValues for a given call instruction. + */ + public List getCallArguments(AbstractInsnNode insn) { + return callArguments.getOrDefault(insn, Collections.emptyList()); + } + + @Override + public SourceValue copyOperation(AbstractInsnNode insn, SourceValue value) { + // Propagate sources through loads/stores instead of creating a new source. + // However, if the value has no sources (e.g. a method parameter), fall back to + // the default behavior so the ALOAD instruction itself is recorded as the source. + if (value.insns.isEmpty()) { + return super.copyOperation(insn, value); + } + return value; + } + + @Override + public SourceValue naryOperation(AbstractInsnNode insn, List values) { + callArguments.put(insn, new ArrayList<>(values)); + return super.naryOperation(insn, values); + } +} From 784b914a43c527a68631392002c96d99f10e4195 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:26:57 -0500 Subject: [PATCH 20/52] Optimize runs of ICapabilityProvider calls into hash lookups --- ...CapabilityProviderDispatcherGenerator.java | 331 +++++++++++++----- 1 file changed, 243 insertions(+), 88 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java index 05dc3c4d..2e460737 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -18,7 +18,11 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -31,6 +35,23 @@ import static org.objectweb.asm.Opcodes.*; * and performs direct dispatch instead of megamorphic virtual calls. */ public class CapabilityProviderDispatcherGenerator { + /** + * Describes the dispatch strategy for a single capability provider in the generated class. + */ + sealed interface ProviderDispatch { + /** Provider handles a known capability - emit an identity guard before dispatch. */ + record Guarded(int providerIndex, String fieldDesc, CapabilityRef capability) implements ProviderDispatch {} + /** Provider capabilities are unknown - dispatch unconditionally. */ + record Unguarded(int providerIndex, String fieldDesc) implements ProviderDispatch {} + /** Multiple guarded dispatches collapsed into a Map lookup. */ + record Hash(int mapIndex, List entries) implements ProviderDispatch {} + } + + /** + * Number of consecutive equality checks that must be performed to switch to a hash map. + */ + private static final int HASH_DISPATCH_THRESHOLD = 3; + private static final String GENERATED_CLASSES_FOLDER = System.getProperty("modernfix.generatedCapabilityDispatcherClassDumpFolder", ""); private static final ConcurrentHashMap>, MethodHandle> cache = @@ -44,6 +65,7 @@ public class CapabilityProviderDispatcherGenerator { private static final String CAPABILITY_DESC = "Lnet/minecraftforge/common/capabilities/Capability;"; private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;"; private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;"; + private static final String MAP_DESC = "Ljava/util/Map;"; /** * Gets or generates a constructor MethodHandle for the given capability provider types. @@ -124,8 +146,122 @@ public class CapabilityProviderDispatcherGenerator { } } + /** + * Build the dispatch list describing how each provider should be handled. + */ + static List buildDispatchList(List> providerTypes, List analysisResults) { + List dispatches = new ArrayList<>(providerTypes.size()); + for (int i = 0; i < providerTypes.size(); i++) { + Class type = providerTypes.get(i); + String fieldDesc = (!type.isHidden() && Modifier.isPublic(type.getModifiers())) + ? Type.getDescriptor(type) : ICAP_PROVIDER_DESC; + + CapabilityAnalysisResult analysis = analysisResults.get(i); + if (analysis instanceof CapabilityAnalysisResult.AlwaysEmpty) { + // No dispatch needed - provider never returns a capability + } else if (analysis instanceof CapabilityAnalysisResult.KnownCapabilities known + && known.capabilities().size() <= 5) { + for (CapabilityRef ref : known.capabilities()) { + dispatches.add(new ProviderDispatch.Guarded(i, fieldDesc, ref)); + } + } else { + dispatches.add(new ProviderDispatch.Unguarded(i, fieldDesc)); + } + } + return dispatches; + } + + /** + * Collapse runs of 3+ consecutive Guarded dispatches into Hash dispatches. + * Duplicate CapabilityRefs within a run are kept as trailing Guarded entries + * after the Hash to preserve sequential fallthrough semantics. + */ + static List optimizeDispatches(List dispatches) { + List result = new ArrayList<>(dispatches.size()); + int mapIndex = 0; + int i = 0; + while (i < dispatches.size()) { + // Collect a run of consecutive Guarded entries + int runStart = i; + while (i < dispatches.size() && dispatches.get(i) instanceof ProviderDispatch.Guarded) { + i++; + } + + List run = dispatches.subList(runStart, i); + if (run.isEmpty()) { + // Not a Guarded entry, pass through + result.add(dispatches.get(i)); + i++; + continue; + } + + if (!tryCollapseToHash(run, mapIndex, result)) { + result.addAll(run); + } else { + mapIndex++; + } + } + return result; + } + + /** + * Attempt to collapse a run of Guarded dispatches into a Hash. + * Returns true if a Hash was emitted, false if the run should be kept as-is. + */ + private static boolean tryCollapseToHash(List run, int mapIndex, List result) { + if (run.size() < HASH_DISPATCH_THRESHOLD) { + return false; + } + + // Deduplicate by CapabilityRef - first occurrence goes into the hash, + // duplicates are kept as trailing Guarded entries for fallthrough + Set seen = new HashSet<>(); + List hashEntries = new ArrayList<>(); + List duplicates = new ArrayList<>(); + for (ProviderDispatch dispatch : run) { + ProviderDispatch.Guarded g = (ProviderDispatch.Guarded) dispatch; + if (seen.add(g.capability())) { + hashEntries.add(g); + } else { + duplicates.add(g); + } + } + + if (hashEntries.size() < HASH_DISPATCH_THRESHOLD) { + return false; + } + + result.add(new ProviderDispatch.Hash(mapIndex, hashEntries)); + result.addAll(duplicates); + return true; + } + + /** + * Collect all unique provider fields (index → fieldDesc) referenced by a dispatch list, + * including those inside Hash entries. + */ + private static LinkedHashMap collectProviderFields(List dispatches) { + LinkedHashMap fields = new LinkedHashMap<>(); + for (ProviderDispatch dispatch : dispatches) { + if (dispatch instanceof ProviderDispatch.Guarded g) { + fields.putIfAbsent(g.providerIndex(), g.fieldDesc()); + } else if (dispatch instanceof ProviderDispatch.Unguarded u) { + fields.putIfAbsent(u.providerIndex(), u.fieldDesc()); + } + // Hash entries don't need provider fields - map reads from constructor array + } + return fields; + } + private static byte[] generateClassBytes(String className, List> providerTypes, List analysisResults) { - ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + List dispatches = optimizeDispatches(buildDispatchList(providerTypes, analysisResults)); + + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) { + @Override + protected ClassLoader getClassLoader() { + return CapabilityProviderDispatcherGenerator.class.getClassLoader(); + } + }; // Class declaration: implements ICapabilityProvider cw.visit( @@ -137,67 +273,89 @@ public class CapabilityProviderDispatcherGenerator { new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" } ); - // Compute field descriptors: use concrete type when possible for JIT devirtualization - String[] fieldDescs = new String[providerTypes.size()]; - for (int i = 0; i < providerTypes.size(); i++) { - Class type = providerTypes.get(i); - fieldDescs[i] = (!type.isHidden() && Modifier.isPublic(type.getModifiers())) - ? Type.getDescriptor(type) : ICAP_PROVIDER_DESC; + // Generate final fields for each distinct provider + LinkedHashMap providerFields = collectProviderFields(dispatches); + for (var entry : providerFields.entrySet()) { + cw.visitField(ACC_PRIVATE | ACC_FINAL, "provider" + entry.getKey(), entry.getValue(), null, null).visitEnd(); } - // Generate final fields for each provider - for (int i = 0; i < providerTypes.size(); i++) { - cw.visitField( - ACC_PRIVATE | ACC_FINAL, - "provider" + i, - fieldDescs[i], - null, - null - ).visitEnd(); + // Generate map fields for Hash dispatches + for (ProviderDispatch dispatch : dispatches) { + if (dispatch instanceof ProviderDispatch.Hash hash) { + cw.visitField(ACC_PRIVATE | ACC_FINAL, "capMap" + hash.mapIndex(), MAP_DESC, null, null).visitEnd(); + } } // Generate constructor - generateConstructor(cw, className, providerTypes.size(), fieldDescs); + generateConstructor(cw, className, providerFields, dispatches); // Generate getCapability method with sided parameter - generateGetCapabilityMethod(cw, className, fieldDescs, analysisResults); + generateGetCapabilityMethod(cw, className, dispatches); cw.visitEnd(); return cw.toByteArray(); } - private static void generateConstructor(ClassWriter cw, String className, int providerCount, String[] fieldDescs) { + private static void generateConstructor(ClassWriter cw, String className, Map providerFields, List dispatches) { Method constructor = Method.getMethod("void (net.minecraftforge.common.capabilities.ICapabilityProvider[])"); GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw); + Type classType = Type.getObjectType(className.replace('.', '/')); // Call super constructor mg.loadThis(); mg.invokeConstructor(Type.getType(Object.class), Method.getMethod("void ()")); - // Unpack array into final fields - for (int i = 0; i < providerCount; i++) { - Type fieldType = Type.getType(fieldDescs[i]); - mg.loadThis(); // this + // Unpack array into provider fields + for (var entry : providerFields.entrySet()) { + int idx = entry.getKey(); + String desc = entry.getValue(); + Type fieldType = Type.getType(desc); + mg.loadThis(); mg.loadArg(0); // array - mg.push(i); // index - mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); // array[i] - if (!fieldDescs[i].equals(ICAP_PROVIDER_DESC)) { + mg.push(idx); // index + mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); + if (!desc.equals(ICAP_PROVIDER_DESC)) { mg.checkCast(fieldType); } - mg.putField( - Type.getObjectType(className.replace('.', '/')), - "provider" + i, - fieldType - ); + mg.putField(classType, "provider" + idx, fieldType); + } + + // Build hash maps + for (ProviderDispatch dispatch : dispatches) { + if (dispatch instanceof ProviderDispatch.Hash hash) { + generateMapConstruction(mg, classType, hash); + } } mg.returnValue(); mg.endMethod(); } - private static void generateGetCapabilityMethod(ClassWriter cw, String className, String[] fieldDescs, List analysisResults) { - int providerCount = fieldDescs.length; + private static void generateMapConstruction(GeneratorAdapter mg, Type classType, ProviderDispatch.Hash hash) { + List entries = hash.entries(); + mg.loadThis(); // for PUTFIELD at the end + mg.push(entries.size()); + mg.visitTypeInsn(ANEWARRAY, "java/util/Map$Entry"); + for (int i = 0; i < entries.size(); i++) { + ProviderDispatch.Guarded g = entries.get(i); + mg.dup(); + mg.push(i); + mg.visitFieldInsn(GETSTATIC, g.capability().owner(), g.capability().fieldName(), CAPABILITY_DESC); + mg.loadArg(0); + mg.push(g.providerIndex()); + mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); + mg.visitMethodInsn(INVOKESTATIC, "java/util/Map", "entry", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/Map$Entry;", true); + mg.visitInsn(AASTORE); + } + mg.visitMethodInsn(INVOKESTATIC, "java/util/Map", "ofEntries", + "([Ljava/util/Map$Entry;)Ljava/util/Map;", true); + + mg.putField(classType, "capMap" + hash.mapIndex(), Type.getType(MAP_DESC)); + } + + private static void generateGetCapabilityMethod(ClassWriter cw, String className, List dispatches) { // Method: LazyOptional getCapability(Capability, Direction) MethodVisitor mv = cw.visitMethod( ACC_PUBLIC, @@ -213,76 +371,73 @@ public class CapabilityProviderDispatcherGenerator { // For each provider, call getCapability and check if present Label endLabel = new Label(); - for (int i = 0; i < providerCount; i++) { - CapabilityAnalysisResult analysis = analysisResults.get(i); + String internalName = className.replace('.', '/'); + String getCapDesc = "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC; + + for (ProviderDispatch dispatch : dispatches) { Label nextLabel = new Label(); - // AlwaysEmpty: skip code generation for this provider entirely - if (analysis instanceof CapabilityAnalysisResult.AlwaysEmpty) { - continue; - } + if (dispatch instanceof ProviderDispatch.Hash hash) { + // ICapabilityProvider p = (ICapabilityProvider) this.capMapN.get(cap); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, internalName, "capMap" + hash.mapIndex(), MAP_DESC); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get", + "(Ljava/lang/Object;)Ljava/lang/Object;", true); + mv.visitVarInsn(ASTORE, 3); - // KnownCapabilities: emit guard checks before dispatch - if (analysis instanceof CapabilityAnalysisResult.KnownCapabilities known - && known.capabilities().size() <= 5) { - if (known.capabilities().size() == 1) { - // Single cap: if (cap != KNOWN_CAP) goto nextProvider - CapabilityRef ref = known.capabilities().iterator().next(); - mv.visitVarInsn(ALOAD, 1); // cap parameter + // if (p == null) goto next + mv.visitVarInsn(ALOAD, 3); + mv.visitJumpInsn(IFNULL, nextLabel); + + // result = ((ICapabilityProvider) p).getCapability(cap, side) + mv.visitVarInsn(ALOAD, 3); + mv.visitTypeInsn(CHECKCAST, "net/minecraftforge/common/capabilities/ICapabilityProvider"); + mv.visitVarInsn(ALOAD, 1); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEINTERFACE, + "net/minecraftforge/common/capabilities/ICapabilityProvider", + "getCapability", getCapDesc, true); + mv.visitVarInsn(ASTORE, 3); + } else { + if (dispatch instanceof ProviderDispatch.Guarded guarded) { + // if (cap != KNOWN_CAP) goto next + CapabilityRef ref = guarded.capability(); + mv.visitVarInsn(ALOAD, 1); mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); mv.visitJumpInsn(IF_ACMPNE, nextLabel); - } else { - // Multiple caps: check each, jump to callProvider on match - Label callProvider = new Label(); - for (CapabilityRef ref : known.capabilities()) { - mv.visitVarInsn(ALOAD, 1); // cap parameter - mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); - mv.visitJumpInsn(IF_ACMPEQ, callProvider); - } - // No match, skip this provider - mv.visitJumpInsn(GOTO, nextLabel); - mv.visitLabel(callProvider); } + + // LazyOptional result = this.providerN.getCapability(cap, side); + int provIdx; + String fDesc; + if (dispatch instanceof ProviderDispatch.Guarded g) { + provIdx = g.providerIndex(); fDesc = g.fieldDesc(); + } else { + var u = (ProviderDispatch.Unguarded) dispatch; + provIdx = u.providerIndex(); fDesc = u.fieldDesc(); + } + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, internalName, "provider" + provIdx, fDesc); + mv.visitVarInsn(ALOAD, 1); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEINTERFACE, + "net/minecraftforge/common/capabilities/ICapabilityProvider", + "getCapability", getCapDesc, true); + mv.visitVarInsn(ASTORE, 3); } - // Indeterminate: no guard, fall through to dispatch - // LazyOptional result = this.providerN.getCapability(cap, side); - mv.visitVarInsn(ALOAD, 0); // this - mv.visitFieldInsn( - GETFIELD, - className.replace('.', '/'), - "provider" + i, - fieldDescs[i] - ); - mv.visitVarInsn(ALOAD, 1); // cap parameter - mv.visitVarInsn(ALOAD, 2); // side parameter - mv.visitMethodInsn( - INVOKEINTERFACE, - "net/minecraftforge/common/capabilities/ICapabilityProvider", - "getCapability", - "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC, - true - ); - - // Store result in local variable - mv.visitVarInsn(ASTORE, 3); - - // if (result == null) continue to next; + // if (result == null) goto next mv.visitVarInsn(ALOAD, 3); mv.visitJumpInsn(IFNULL, nextLabel); - // if (result.isPresent()) return result; + // if (result.isPresent()) return result mv.visitVarInsn(ALOAD, 3); - mv.visitMethodInsn( - INVOKEVIRTUAL, + mv.visitMethodInsn(INVOKEVIRTUAL, "net/minecraftforge/common/util/LazyOptional", - "isPresent", - "()Z", - false - ); + "isPresent", "()Z", false); mv.visitJumpInsn(IFEQ, nextLabel); - // return result mv.visitVarInsn(ALOAD, 3); mv.visitInsn(ARETURN); From e16179b7972c38b973938377af543eb067f8f6d7 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:08:06 -0500 Subject: [PATCH 21/52] Emit more debug info to the generated dispatcher classes --- ...CapabilityProviderDispatcherGenerator.java | 49 ++++++++++++++----- .../forge/capability/OriginalType.java | 17 +++++++ 2 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/embeddedt/modernfix/forge/capability/OriginalType.java diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java index 2e460737..c5c5c10e 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -66,6 +66,7 @@ public class CapabilityProviderDispatcherGenerator { private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;"; private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;"; private static final String MAP_DESC = "Ljava/util/Map;"; + private static final String MAP_SIGNATURE = "Ljava/util/Map;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;>;"; /** * Gets or generates a constructor MethodHandle for the given capability provider types. @@ -276,13 +277,20 @@ public class CapabilityProviderDispatcherGenerator { // Generate final fields for each distinct provider LinkedHashMap providerFields = collectProviderFields(dispatches); for (var entry : providerFields.entrySet()) { - cw.visitField(ACC_PRIVATE | ACC_FINAL, "provider" + entry.getKey(), entry.getValue(), null, null).visitEnd(); + FieldVisitor fv = cw.visitField(ACC_PRIVATE | ACC_FINAL, "provider" + entry.getKey(), entry.getValue(), null, null); + if (entry.getValue().equals(ICAP_PROVIDER_DESC)) { + String originalName = providerTypes.get(entry.getKey()).getName(); + AnnotationVisitor av = fv.visitAnnotation("Lorg/embeddedt/modernfix/forge/capability/OriginalType;", false); + av.visit("value", originalName); + av.visitEnd(); + } + fv.visitEnd(); } // Generate map fields for Hash dispatches for (ProviderDispatch dispatch : dispatches) { if (dispatch instanceof ProviderDispatch.Hash hash) { - cw.visitField(ACC_PRIVATE | ACC_FINAL, "capMap" + hash.mapIndex(), MAP_DESC, null, null).visitEnd(); + cw.visitField(ACC_PRIVATE | ACC_FINAL, "capMap" + hash.mapIndex(), MAP_DESC, MAP_SIGNATURE, null).visitEnd(); } } @@ -367,32 +375,36 @@ public class CapabilityProviderDispatcherGenerator { mv.visitCode(); - // Generate unrolled dispatch loop - // For each provider, call getCapability and check if present - Label endLabel = new Label(); + Label methodStart = new Label(); + Label methodEnd = new Label(); + mv.visitLabel(methodStart); String internalName = className.replace('.', '/'); String getCapDesc = "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC; + // slot 3 = LazyOptional result (all paths) + // slot 4 = ICapabilityProvider provider (Hash paths only) + boolean usesProviderLocal = dispatches.stream().anyMatch(d -> d instanceof ProviderDispatch.Hash); + for (ProviderDispatch dispatch : dispatches) { Label nextLabel = new Label(); if (dispatch instanceof ProviderDispatch.Hash hash) { - // ICapabilityProvider p = (ICapabilityProvider) this.capMapN.get(cap); + // ICapabilityProvider provider = (ICapabilityProvider) this.capMapN.get(cap); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, internalName, "capMap" + hash.mapIndex(), MAP_DESC); mv.visitVarInsn(ALOAD, 1); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true); - mv.visitVarInsn(ASTORE, 3); + mv.visitTypeInsn(CHECKCAST, "net/minecraftforge/common/capabilities/ICapabilityProvider"); + mv.visitVarInsn(ASTORE, 4); - // if (p == null) goto next - mv.visitVarInsn(ALOAD, 3); + // if (provider == null) goto next + mv.visitVarInsn(ALOAD, 4); mv.visitJumpInsn(IFNULL, nextLabel); - // result = ((ICapabilityProvider) p).getCapability(cap, side) - mv.visitVarInsn(ALOAD, 3); - mv.visitTypeInsn(CHECKCAST, "net/minecraftforge/common/capabilities/ICapabilityProvider"); + // LazyOptional result = provider.getCapability(cap, side) + mv.visitVarInsn(ALOAD, 4); mv.visitVarInsn(ALOAD, 1); mv.visitVarInsn(ALOAD, 2); mv.visitMethodInsn(INVOKEINTERFACE, @@ -445,7 +457,6 @@ public class CapabilityProviderDispatcherGenerator { } // If no provider returned a capability, return empty - mv.visitLabel(endLabel); mv.visitMethodInsn( INVOKESTATIC, "net/minecraftforge/common/util/LazyOptional", @@ -455,6 +466,18 @@ public class CapabilityProviderDispatcherGenerator { ); mv.visitInsn(ARETURN); + mv.visitLabel(methodEnd); + + // Local variable table for clean decompilation + String capSig = CAPABILITY_DESC.replace(";", ";"); + String resultSig = LAZY_OPTIONAL_DESC.replace(";", ";"); + mv.visitLocalVariable("cap", CAPABILITY_DESC, capSig, methodStart, methodEnd, 1); + mv.visitLocalVariable("side", DIRECTION_DESC, null, methodStart, methodEnd, 2); + mv.visitLocalVariable("result", LAZY_OPTIONAL_DESC, resultSig, methodStart, methodEnd, 3); + if (usesProviderLocal) { + mv.visitLocalVariable("provider", ICAP_PROVIDER_DESC, null, methodStart, methodEnd, 4); + } + mv.visitMaxs(0, 0); // Computed by COMPUTE_MAXS mv.visitEnd(); } diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/OriginalType.java b/src/main/java/org/embeddedt/modernfix/forge/capability/OriginalType.java new file mode 100644 index 00000000..cf2143cf --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/OriginalType.java @@ -0,0 +1,17 @@ +package org.embeddedt.modernfix.forge.capability; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Applied to generated provider fields whose declared type has been widened to + * {@link net.minecraftforge.common.capabilities.ICapabilityProvider} because the + * concrete class is non-public or hidden. The value records the original type name. + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.FIELD) +public @interface OriginalType { + String value(); +} From 60850610f9510003e9eb9d9af19e5122fb8bd0ad Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:11:24 -0500 Subject: [PATCH 22/52] Group capability providers of known types together when possible --- .../AttachCapabilitiesEventMixin.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/bytecode_analysis/AttachCapabilitiesEventMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/bytecode_analysis/AttachCapabilitiesEventMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/bytecode_analysis/AttachCapabilitiesEventMixin.java new file mode 100644 index 00000000..a5c168c4 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/bytecode_analysis/AttachCapabilitiesEventMixin.java @@ -0,0 +1,70 @@ +package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities.bytecode_analysis; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.eventbus.api.EventPriority; +import org.apache.commons.lang3.tuple.Pair; +import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalysisResult; +import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalyzer; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@Mixin(AttachCapabilitiesEvent.class) +public abstract class AttachCapabilitiesEventMixin extends Event { + @Shadow + public abstract void addCapability(ResourceLocation key, ICapabilityProvider cap); + + @Unique + private static final EventPriority MFIX_LAST_PRIO = EventPriority.values()[EventPriority.values().length - 1]; + + @Unique + private final List> mfix$batchedCaps = new ArrayList<>(); + + private static final Comparator> MFIX_COMPARATOR = Comparator.comparingInt(pair -> { + var result = CapabilityAnalyzer.analyze(pair.getRight().getClass()); + return result instanceof CapabilityAnalysisResult.Indeterminate ? 1 : 0; + }); + + @Unique + private boolean insertingBatch; + + /** + * @author embeddedt + * @reason batch additions of capability providers within the same phase so that we can hoist all + * the ones with statically known capability types to the beginning of the provider list + */ + @WrapMethod(method = "addCapability", remap = false) + private void mfix$batchCaps(ResourceLocation key, ICapabilityProvider cap, Operation original) { + // For simplicity, we don't try to batch on the last phase + if (this.insertingBatch || this.getPhase() == MFIX_LAST_PRIO) { + original.call(key, cap); + } else { + mfix$batchedCaps.add(Pair.of(key, cap)); + } + } + + @Override + public void setPhase(@NotNull EventPriority value) { + if (!this.mfix$batchedCaps.isEmpty()) { + this.mfix$batchedCaps.sort(MFIX_COMPARATOR); + this.insertingBatch = true; + try { + this.mfix$batchedCaps.forEach(p -> this.addCapability(p.getKey(), p.getValue())); + } finally { + this.insertingBatch = false; + this.mfix$batchedCaps.clear(); + } + } + super.setPhase(value); + } +} From e63d99763e3dc44569088a75937fa6c7380fb15c Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:29:16 -0500 Subject: [PATCH 23/52] Avoid initializing lazy capability providers for compatibility checks where possible --- .../CapabilityProviderMixin.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityProviderMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityProviderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityProviderMixin.java new file mode 100644 index 00000000..6e68f94a --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/CapabilityProviderMixin.java @@ -0,0 +1,39 @@ +package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.nbt.CompoundTag; +import net.minecraftforge.common.capabilities.CapabilityProvider; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Objects; + +@Mixin(value = CapabilityProvider.class, remap = false) +@SuppressWarnings("rawtypes") +public class CapabilityProviderMixin { + @Shadow + private boolean isLazy; + + @Shadow + private boolean initialized; + + @Shadow + private CompoundTag lazyData; + + /** + * @author embeddedt + * @reason avoid initializing capabilities in the case where we can reasonably expect the lazy data + * to deserialize to the same concrete caps + */ + @WrapMethod(method = "areCapsCompatible(Lnet/minecraftforge/common/capabilities/CapabilityProvider;)Z") + private boolean mfix$skipLazyInit(CapabilityProvider other, Operation original) { + if (this.isLazy && !this.initialized) { + var otherAcc = ((CapabilityProviderMixin)(Object)other); + if (otherAcc.isLazy && !otherAcc.initialized && otherAcc.getClass() == this.getClass() && Objects.equals(this.lazyData, otherAcc.lazyData)) { + return true; + } + } + return original.call(other); + } +} From 696b344ef5f02471b399d453f511943edcc39d94 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:35:58 -0500 Subject: [PATCH 24/52] Fix missed detection of certain cap equality checks --- .../analysis/CapabilityAnalyzer.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java index bcd8e1ae..4ccd22d2 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java @@ -232,6 +232,9 @@ public class CapabilityAnalyzer { } } + if (source instanceof MethodInsnNode m) { + return new ReturnClassification.Unknown("unclassified method: " + m.owner + "." + m.name + m.desc); + } return new ReturnClassification.Unknown("unclassified source: " + source.getClass().getSimpleName() + " opcode=" + source.getOpcode()); } @@ -315,6 +318,28 @@ public class CapabilityAnalyzer { } } + // Extend guard regions for forward jumps that land beyond the guard target. + // This handles compound conditions like (cap == X && cond) compiled as: + // if_acmpne L_false // guard: [here, L_false) + // evaluate cond + // ifeq L_true // forward jump beyond L_false + // L_false: empty(); areturn + // L_true: cast(); areturn // <-- also guarded by cap == X + int baseSize = regions.size(); + for (int r = 0; r < baseSize; r++) { + GuardRegion guard = regions.get(r); + for (int j = guard.guardIndex + 1; j < guard.targetIndex; j++) { + AbstractInsnNode inner = instructions.get(j); + if (inner instanceof JumpInsnNode jump) { + int jumpTarget = instructions.indexOf(jump.label); + if (jumpTarget >= guard.targetIndex) { + int endIndex = findGuardedRegionEnd(instructions, jumpTarget); + regions.add(new GuardRegion(guard.capabilityRef, jumpTarget, endIndex)); + } + } + } + } + return regions; } From df060108465fe9d8ac39371283494ad898058174 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:53:40 -0500 Subject: [PATCH 25/52] Fix superclass capability types being ignored sometimes --- .../analysis/CapabilityAnalyzer.java | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java index 4ccd22d2..72a8ab08 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java @@ -154,31 +154,39 @@ public class CapabilityAnalyzer { // If any source is unknown, try the guard region fallback before giving up if (!unknownSources.isEmpty()) { + boolean allResolved = false; + // Check if the return itself is in a guard region for (GuardRegion guard : guardRegions) { if (returnIndex > guard.guardIndex && returnIndex < guard.targetIndex) { - return new ReturnClassification.Known(Set.of(guard.capabilityRef)); - } - } - // Also check if each unknown source instruction is inside a guard region. - // This handles ternary patterns where both branches merge into a single - // ARETURN after the guard, but the unknown value was produced inside it. - boolean allResolved = true; - for (AbstractInsnNode unknownSource : unknownSources) { - int sourceIndex = instructions.indexOf(unknownSource); - boolean resolved = false; - for (GuardRegion guard : guardRegions) { - if (sourceIndex > guard.guardIndex && sourceIndex < guard.targetIndex) { - caps.add(guard.capabilityRef); - resolved = true; - break; - } - } - if (!resolved) { - allResolved = false; + caps.add(guard.capabilityRef); + allResolved = true; break; } } + + // Also check if each unknown source instruction is inside a guard region. + // This handles ternary patterns where both branches merge into a single + // ARETURN after the guard, but the unknown value was produced inside it. + if (!allResolved) { + allResolved = true; + for (AbstractInsnNode unknownSource : unknownSources) { + int sourceIndex = instructions.indexOf(unknownSource); + boolean resolved = false; + for (GuardRegion guard : guardRegions) { + if (sourceIndex > guard.guardIndex && sourceIndex < guard.targetIndex) { + caps.add(guard.capabilityRef); + resolved = true; + break; + } + } + if (!resolved) { + allResolved = false; + break; + } + } + } + if (!allResolved) { return new ReturnClassification.Unknown(unknownReason); } From 15f30b532c1c545692e81b185216920df719d31c Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:30:35 -0500 Subject: [PATCH 26/52] Reduce generated class size slightly --- ...CapabilityProviderDispatcherGenerator.java | 130 +++++++++++------- 1 file changed, 83 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java index c5c5c10e..1c4606d5 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -386,57 +386,19 @@ public class CapabilityProviderDispatcherGenerator { // slot 4 = ICapabilityProvider provider (Hash paths only) boolean usesProviderLocal = dispatches.stream().anyMatch(d -> d instanceof ProviderDispatch.Hash); - for (ProviderDispatch dispatch : dispatches) { + for (int di = 0; di < dispatches.size(); ) { + ProviderDispatch dispatch = dispatches.get(di); Label nextLabel = new Label(); if (dispatch instanceof ProviderDispatch.Hash hash) { - // ICapabilityProvider provider = (ICapabilityProvider) this.capMapN.get(cap); - mv.visitVarInsn(ALOAD, 0); - mv.visitFieldInsn(GETFIELD, internalName, "capMap" + hash.mapIndex(), MAP_DESC); - mv.visitVarInsn(ALOAD, 1); - mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get", - "(Ljava/lang/Object;)Ljava/lang/Object;", true); - mv.visitTypeInsn(CHECKCAST, "net/minecraftforge/common/capabilities/ICapabilityProvider"); - mv.visitVarInsn(ASTORE, 4); - - // if (provider == null) goto next - mv.visitVarInsn(ALOAD, 4); - mv.visitJumpInsn(IFNULL, nextLabel); - - // LazyOptional result = provider.getCapability(cap, side) - mv.visitVarInsn(ALOAD, 4); - mv.visitVarInsn(ALOAD, 1); - mv.visitVarInsn(ALOAD, 2); - mv.visitMethodInsn(INVOKEINTERFACE, - "net/minecraftforge/common/capabilities/ICapabilityProvider", - "getCapability", getCapDesc, true); - mv.visitVarInsn(ASTORE, 3); + emitHashDispatch(mv, internalName, getCapDesc, hash, nextLabel); + di++; + } else if (dispatch instanceof ProviderDispatch.Guarded) { + di = emitGuardedDispatch(mv, internalName, getCapDesc, dispatches, di, nextLabel); } else { - if (dispatch instanceof ProviderDispatch.Guarded guarded) { - // if (cap != KNOWN_CAP) goto next - CapabilityRef ref = guarded.capability(); - mv.visitVarInsn(ALOAD, 1); - mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); - mv.visitJumpInsn(IF_ACMPNE, nextLabel); - } - - // LazyOptional result = this.providerN.getCapability(cap, side); - int provIdx; - String fDesc; - if (dispatch instanceof ProviderDispatch.Guarded g) { - provIdx = g.providerIndex(); fDesc = g.fieldDesc(); - } else { - var u = (ProviderDispatch.Unguarded) dispatch; - provIdx = u.providerIndex(); fDesc = u.fieldDesc(); - } - mv.visitVarInsn(ALOAD, 0); - mv.visitFieldInsn(GETFIELD, internalName, "provider" + provIdx, fDesc); - mv.visitVarInsn(ALOAD, 1); - mv.visitVarInsn(ALOAD, 2); - mv.visitMethodInsn(INVOKEINTERFACE, - "net/minecraftforge/common/capabilities/ICapabilityProvider", - "getCapability", getCapDesc, true); - mv.visitVarInsn(ASTORE, 3); + var u = (ProviderDispatch.Unguarded) dispatch; + emitProviderGetCapability(mv, internalName, getCapDesc, u.providerIndex(), u.fieldDesc()); + di++; } // if (result == null) goto next @@ -482,6 +444,80 @@ public class CapabilityProviderDispatcherGenerator { mv.visitEnd(); } + private static void emitHashDispatch(MethodVisitor mv, String internalName, String getCapDesc, + ProviderDispatch.Hash hash, Label nextLabel) { + // ICapabilityProvider provider = (ICapabilityProvider) this.capMapN.get(cap); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, internalName, "capMap" + hash.mapIndex(), MAP_DESC); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get", + "(Ljava/lang/Object;)Ljava/lang/Object;", true); + mv.visitTypeInsn(CHECKCAST, "net/minecraftforge/common/capabilities/ICapabilityProvider"); + mv.visitVarInsn(ASTORE, 4); + + // if (provider == null) goto next + mv.visitVarInsn(ALOAD, 4); + mv.visitJumpInsn(IFNULL, nextLabel); + + // LazyOptional result = provider.getCapability(cap, side) + mv.visitVarInsn(ALOAD, 4); + mv.visitVarInsn(ALOAD, 1); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEINTERFACE, + "net/minecraftforge/common/capabilities/ICapabilityProvider", + "getCapability", getCapDesc, true); + mv.visitVarInsn(ASTORE, 3); + } + + /** + * Emit guarded dispatch for one or more consecutive Guarded entries sharing the same providerIndex. + * When multiple caps map to the same provider, an OR-chain guard is emitted instead of separate dispatches. + * + * @return the updated dispatch index (past the consumed group) + */ + private static int emitGuardedDispatch(MethodVisitor mv, String internalName, String getCapDesc, + List dispatches, int di, Label nextLabel) { + var guarded = (ProviderDispatch.Guarded) dispatches.get(di); + + // Peek ahead to collect consecutive Guarded entries with same providerIndex + int groupEnd = di + 1; + while (groupEnd < dispatches.size() + && dispatches.get(groupEnd) instanceof ProviderDispatch.Guarded next + && next.providerIndex() == guarded.providerIndex()) { + groupEnd++; + } + + // OR-chain: IF_ACMPEQ matchLabel for each cap except the last, IF_ACMPNE nextLabel for the last + Label matchLabel = new Label(); + for (int gi = di; gi < groupEnd; gi++) { + var g = (ProviderDispatch.Guarded) dispatches.get(gi); + CapabilityRef ref = g.capability(); + mv.visitVarInsn(ALOAD, 1); + mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); + if (gi < groupEnd - 1) { + mv.visitJumpInsn(IF_ACMPEQ, matchLabel); + } else { + mv.visitJumpInsn(IF_ACMPNE, nextLabel); + } + } + mv.visitLabel(matchLabel); + + emitProviderGetCapability(mv, internalName, getCapDesc, guarded.providerIndex(), guarded.fieldDesc()); + return groupEnd; + } + + private static void emitProviderGetCapability(MethodVisitor mv, String internalName, String getCapDesc, + int providerIndex, String fieldDesc) { + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, internalName, "provider" + providerIndex, fieldDesc); + mv.visitVarInsn(ALOAD, 1); + mv.visitVarInsn(ALOAD, 2); + mv.visitMethodInsn(INVOKEINTERFACE, + "net/minecraftforge/common/capabilities/ICapabilityProvider", + "getCapability", getCapDesc, true); + mv.visitVarInsn(ASTORE, 3); + } + private static String formatAnalysisResult(CapabilityAnalysisResult result) { if (result instanceof CapabilityAnalysisResult.AlwaysEmpty) { return "always empty (skipped)"; From 49d800ff27b0f4a724232b9d28cd3abc97e2dcd3 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:19:04 -0500 Subject: [PATCH 27/52] Avoid calling LazyOptional.isPresent() if possible --- ...CapabilityProviderDispatcherGenerator.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java index 1c4606d5..72a10cad 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -1,6 +1,7 @@ package org.embeddedt.modernfix.forge.capability; import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.util.LazyOptional; import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalysisResult; import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalyzer; @@ -54,6 +55,12 @@ public class CapabilityProviderDispatcherGenerator { private static final String GENERATED_CLASSES_FOLDER = System.getProperty("modernfix.generatedCapabilityDispatcherClassDumpFolder", ""); + /** + * Sentinel used in generated guards to skip empty results via reference equality, + * avoiding a method call to {@code isPresent()}. + */ + public static final LazyOptional EMPTY = LazyOptional.empty(); + private static final ConcurrentHashMap>, MethodHandle> cache = new ConcurrentHashMap<>(); @@ -401,9 +408,16 @@ public class CapabilityProviderDispatcherGenerator { di++; } - // if (result == null) goto next - mv.visitVarInsn(ALOAD, 3); - mv.visitJumpInsn(IFNULL, nextLabel); + // If not a hash lookup, then optimistically check for the exact LazyOptional.empty() value to avoid + // an isPresent call + if (!(dispatch instanceof ProviderDispatch.Hash)) { + // if (result == EMPTY) goto next + mv.visitVarInsn(ALOAD, 3); + mv.visitFieldInsn(GETSTATIC, + "org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator", + "EMPTY", LAZY_OPTIONAL_DESC); + mv.visitJumpInsn(IF_ACMPEQ, nextLabel); + } // if (result.isPresent()) return result mv.visitVarInsn(ALOAD, 3); From ee34dcf96eac75814be68b34ccf44002ad3b632f Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 28 Feb 2026 16:42:42 -0500 Subject: [PATCH 28/52] Drastically simplify and document chunk system memory usage patch --- .../paper_chunk_patches/ChunkMapMixin.java | 61 ++++++------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/paper_chunk_patches/ChunkMapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/paper_chunk_patches/ChunkMapMixin.java index a6d8d48e..742f6b01 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/paper_chunk_patches/ChunkMapMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/paper_chunk_patches/ChunkMapMixin.java @@ -1,22 +1,18 @@ package org.embeddedt.modernfix.common.mixin.bugfix.paper_chunk_patches; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.mojang.datafixers.util.Either; import net.minecraft.server.level.*; -import net.minecraft.server.level.progress.ChunkProgressListener; import net.minecraft.util.thread.BlockableEventLoop; -import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkStatus; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyArg; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -25,18 +21,6 @@ import java.util.concurrent.Executor; public abstract class ChunkMapMixin { @Shadow @Final private BlockableEventLoop mainThreadExecutor; - @Shadow @Final private ChunkMap.DistanceManager distanceManager; - - @Shadow protected abstract CompletableFuture> protoChunkToFullChunk(ChunkHolder arg); - - @Shadow @Final private ServerLevel level; - @Shadow @Final private ThreadedLevelLightEngine lightEngine; - @Shadow @Final private ChunkProgressListener progressListener; - - @Shadow protected abstract CompletableFuture> scheduleChunkGeneration(ChunkHolder chunkHolder, ChunkStatus chunkStatus); - - @Shadow @Final private StructureTemplateManager structureTemplateManager; - /* https://github.com/PaperMC/Paper/blob/ver/1.17.1/patches/server/0752-Fix-chunks-refusing-to-unload-at-low-TPS.patch */ @ModifyArg(method = "prepareAccessibleChunk", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 1) private Executor useMainThreadExecutor(Executor executor) { @@ -45,31 +29,24 @@ public abstract class ChunkMapMixin { /** * @author embeddedt - * @reason revert 1.17 chunk system changes, significantly reduces time and RAM needed to load chunks + * @reason 1.17+ uses getNow to check if the parent future is ready, and calls scheduleChunkGeneration as soon as + * it is found to not be ready. In the latter scenario, a massive number of extra CompletableFutures are allocated + * even if they are not actually necessary if the future is waited for. To prevent this, if the parent future + * is not done, we wait for it to complete and then retry schedule(). This will either detect an adequate + * status and return a loading future, or re-enter this injector with the parent future completed, in which case + * we proceed to schedule generation as originally requested. */ - @Inject(method = "schedule", at = @At("HEAD"), cancellable = true) - private void useLegacySchedulingLogic(ChunkHolder holder, ChunkStatus requiredStatus, CallbackInfoReturnable>> cir) { - if(requiredStatus != ChunkStatus.EMPTY && !requiredStatus.hasLoadDependencies()) { - ChunkPos chunkpos = holder.getPos(); - CompletableFuture> future = holder.getOrScheduleFuture(requiredStatus.getParent(), (ChunkMap)(Object)this); - cir.setReturnValue(future.thenComposeAsync((either) -> { - Optional optional = either.left(); - - if (requiredStatus == ChunkStatus.LIGHT) { - this.distanceManager.addTicket(TicketType.LIGHT, chunkpos, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkpos); - } - - // from original method - if (optional.isPresent() && optional.get().getStatus().isOrAfter(requiredStatus)) { - CompletableFuture> completablefuture = requiredStatus.load(this.level, this.structureTemplateManager, this.lightEngine, (arg2) -> { - return this.protoChunkToFullChunk(holder); - }, (ChunkAccess)optional.get()); - this.progressListener.onStatusChange(chunkpos, requiredStatus); - return completablefuture; - } else { - return this.scheduleChunkGeneration(holder, requiredStatus); - } - }, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor)); + @WrapOperation(method = "schedule", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ChunkMap;scheduleChunkGeneration(Lnet/minecraft/server/level/ChunkHolder;Lnet/minecraft/world/level/chunk/ChunkStatus;)Ljava/util/concurrent/CompletableFuture;")) + private CompletableFuture> mfix$avoidSchedulingGenerationPrematurely(ChunkMap map, ChunkHolder holder, ChunkStatus status, Operation>> original) { + if (!status.hasLoadDependencies()) { + var parentFuture = holder.getOrScheduleFuture(status.getParent(), map); + if (!parentFuture.isDone()) { + return parentFuture.thenComposeAsync( + either -> map.schedule(holder, status), + this.mainThreadExecutor + ); + } } + return original.call(map, holder, status); } } From 30e3deb8e248e900e45f096d30021983425f1e68 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 1 Mar 2026 15:18:13 -0500 Subject: [PATCH 29/52] Avoid unnecessary chunkloads when remove_spawn_chunks is enabled --- .../MinecraftServerMixin.java | 83 ++++++++++++++++++- .../remove_spawn_chunks/PlayerListMixin.java | 26 ++++++ .../ServerChunkCacheAccessor.java | 12 --- .../ServerPlayerMixin.java | 20 +++++ .../duck/ISpawnTrackingMinecraftServer.java | 10 +++ 5 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/PlayerListMixin.java delete mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/ServerChunkCacheAccessor.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/ServerPlayerMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/duck/ISpawnTrackingMinecraftServer.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/MinecraftServerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/MinecraftServerMixin.java index 411766e5..f0383155 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/MinecraftServerMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/MinecraftServerMixin.java @@ -1,24 +1,99 @@ package org.embeddedt.modernfix.common.mixin.perf.remove_spawn_chunks; +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.serialization.Dynamic; +import net.minecraft.core.SectionPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.TicketType; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.util.Mth; +import net.minecraft.util.Unit; import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.WorldData; +import org.apache.commons.lang3.tuple.Pair; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.duck.ISpawnTrackingMinecraftServer; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Redirect; +import javax.annotation.Nullable; + @Mixin(value = MinecraftServer.class, priority = 1100) -public class MinecraftServerMixin { +public abstract class MinecraftServerMixin implements ISpawnTrackingMinecraftServer { + @Shadow + public abstract boolean isDedicatedServer(); + + @Shadow + public abstract WorldData getWorldData(); + + @Shadow + @Nullable + public abstract ServerLevel getLevel(ResourceKey dimension); + + private Pair, ChunkPos> mfix$initialSpawnLocation; + + private @Nullable Pair, ChunkPos> loadPlayerSpawnLocation() { + CompoundTag player = this.getWorldData().getLoadedPlayerTag(); + + if (player == null) { + return null; + } + + ListTag pos = player.getList("Pos", CompoundTag.TAG_DOUBLE); + double x = pos.getDouble(0); + double z = pos.getDouble(2); + + // Dimension + ResourceKey dimension = DimensionType.parseLegacy( + new Dynamic<>(NbtOps.INSTANCE, player.get("Dimension")) + ).resultOrPartial(ModernFix.LOGGER::error).orElse(Level.OVERWORLD); + + return Pair.of(dimension, new ChunkPos(SectionPos.blockToSectionCoord(Mth.floor(x)), SectionPos.blockToSectionCoord(Mth.floor(z)))); + } + @Redirect(method = "prepareLevels", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerChunkCache;addRegionTicket(Lnet/minecraft/server/level/TicketType;Lnet/minecraft/world/level/ChunkPos;ILjava/lang/Object;)V")) - private void addSpawnChunkTicket(ServerChunkCache cache, TicketType type, ChunkPos pos, int distance, Object o) { - // load first chunk - cache.getChunk(pos.x, pos.z, ChunkStatus.FULL, true); + private void addSpawnChunkTicket(ServerChunkCache cache, TicketType type, ChunkPos pos, int distance, Object o, @Local(ordinal = 0, argsOnly = true) ChunkProgressListener listener) { + if (!this.isDedicatedServer()) { + // Temporarily create a START ticket around the player to load the world in parallel with client join + // We remove it once the player has joined the world + var pair = this.mfix$initialSpawnLocation = loadPlayerSpawnLocation(); + if (pair != null) { + var level = this.getLevel(pair.getLeft()); + if (level != null) { + cache = level.getChunkSource(); + pos = pair.getRight(); + } + } + + listener.updateSpawnPos(pos); + cache.addRegionTicket(TicketType.START, pos, 0, Unit.INSTANCE); + } else { + // just trigger sync load of initial spawn once + // TODO: figure out if this magic is still needed + cache.getChunk(pos.x, pos.z, true); + } } @Redirect(method = "prepareLevels", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerChunkCache;getTickingGenerated()I"), require = 0) private int getGenerated(ServerChunkCache cache) { return 441; } + + @Override + public Pair, ChunkPos> mfix$getInitialStartTicketLocation() { + var pair = this.mfix$initialSpawnLocation; + this.mfix$initialSpawnLocation = null; + return pair; + } } diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/PlayerListMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/PlayerListMixin.java new file mode 100644 index 00000000..b5c4f746 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/PlayerListMixin.java @@ -0,0 +1,26 @@ +package org.embeddedt.modernfix.common.mixin.perf.remove_spawn_chunks; + +import net.minecraft.network.Connection; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.server.players.PlayerList; +import net.minecraft.util.Unit; +import org.embeddedt.modernfix.duck.ISpawnTrackingMinecraftServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerList.class) +public class PlayerListMixin { + @Inject(method = "placeNewPlayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerLevel;addNewPlayer(Lnet/minecraft/server/level/ServerPlayer;)V", shift = At.Shift.AFTER)) + private void removeStartTicket(Connection netManager, ServerPlayer player, CallbackInfo ci) { + var initial = ((ISpawnTrackingMinecraftServer)player.server).mfix$getInitialStartTicketLocation(); + if (initial != null) { + var level = player.server.getLevel(initial.getLeft()); + if (level != null) { + level.getChunkSource().removeRegionTicket(TicketType.START, initial.getRight(), 0, Unit.INSTANCE); + } + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/ServerChunkCacheAccessor.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/ServerChunkCacheAccessor.java deleted file mode 100644 index 1ae8cbaa..00000000 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/ServerChunkCacheAccessor.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.perf.remove_spawn_chunks; - -import net.minecraft.server.level.DistanceManager; -import net.minecraft.server.level.ServerChunkCache; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -@Mixin(ServerChunkCache.class) -public interface ServerChunkCacheAccessor { - @Accessor("distanceManager") - DistanceManager getDistanceManager(); -} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/ServerPlayerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/ServerPlayerMixin.java new file mode 100644 index 00000000..74eda4a7 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/remove_spawn_chunks/ServerPlayerMixin.java @@ -0,0 +1,20 @@ +package org.embeddedt.modernfix.common.mixin.perf.remove_spawn_chunks; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ServerPlayer.class) +public class ServerPlayerMixin { + /** + * @author embeddedt + * @reason do not waste time loading the wrong chunks and placing the player there just to correct it later + */ + @WrapWithCondition(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;fudgeSpawnLocation(Lnet/minecraft/server/level/ServerLevel;)V")) + private boolean skipFudgingForSPOwner(ServerPlayer player, ServerLevel targetLevel) { + return targetLevel.getServer().getWorldData().getLoadedPlayerTag() == null + || !targetLevel.getServer().isSingleplayerOwner(player.getGameProfile()); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/duck/ISpawnTrackingMinecraftServer.java b/src/main/java/org/embeddedt/modernfix/duck/ISpawnTrackingMinecraftServer.java new file mode 100644 index 00000000..46fb5b0b --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/ISpawnTrackingMinecraftServer.java @@ -0,0 +1,10 @@ +package org.embeddedt.modernfix.duck; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import org.apache.commons.lang3.tuple.Pair; + +public interface ISpawnTrackingMinecraftServer { + Pair, ChunkPos> mfix$getInitialStartTicketLocation(); +} From 925c7526ee36fb5d280ab16ecf7d2d6cd37f7b2c Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 1 Mar 2026 15:46:52 -0500 Subject: [PATCH 30/52] Reduce memory usage of ImposterProtoChunks --- .../ChunkAccessMixin.java | 22 +++++++++++++++++++ .../ImposterProtoChunkMixin.java | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ChunkAccessMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ImposterProtoChunkMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ChunkAccessMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ChunkAccessMixin.java new file mode 100644 index 00000000..0343f25b --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ChunkAccessMixin.java @@ -0,0 +1,22 @@ +package org.embeddedt.modernfix.common.mixin.perf.compact_imposterprotochunks; + +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.lighting.ChunkSkyLightSources; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; + +@Mixin(ChunkAccess.class) +public class ChunkAccessMixin { + @Shadow + @Final + @Mutable + protected LevelChunkSection[] sections; + + @Shadow + protected ChunkSkyLightSources skyLightSources; +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ImposterProtoChunkMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ImposterProtoChunkMixin.java new file mode 100644 index 00000000..36829c70 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ImposterProtoChunkMixin.java @@ -0,0 +1,22 @@ +package org.embeddedt.modernfix.common.mixin.perf.compact_imposterprotochunks; + +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ImposterProtoChunk.class) +public abstract class ImposterProtoChunkMixin extends ChunkAccessMixin { + /** + * @author embeddedt + * @reason ImposterProtoChunks allocate their own LevelChunkSection objects etc. which wastes quite + * a bit of memory + */ + @Inject(method = "", at = @At("RETURN")) + private void replaceDuplicateObjects(LevelChunk wrapped, boolean allowWrites, CallbackInfo ci) { + this.sections = wrapped.getSections(); + this.skyLightSources = wrapped.getSkyLightSources(); + } +} From 21cbcb0e0491a82b21cd71f168cc560768aef797 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:52:13 -0500 Subject: [PATCH 31/52] Strip signatures from jar manifests at startup to save memory --- .../common/mixin/core/BootstrapMixin.java | 2 ++ .../forge/classloading/ManifestCompactor.java | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/forge/classloading/ManifestCompactor.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java index 52e582ba..1a108612 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java @@ -2,6 +2,7 @@ package org.embeddedt.modernfix.common.mixin.core; import net.minecraft.server.Bootstrap; import net.minecraftforge.network.NetworkConstants; +import org.embeddedt.modernfix.forge.classloading.ManifestCompactor; import org.slf4j.Logger; import org.embeddedt.modernfix.forge.load.ModWorkManagerQueue; import org.embeddedt.modernfix.util.TimeFormatter; @@ -25,6 +26,7 @@ public class BootstrapMixin { if(!isBootstrapped) { LOGGER.info("ModernFix reached bootstrap stage ({} after launch)", TimeFormatter.formatNanos(ManagementFactory.getRuntimeMXBean().getUptime() * 1000L * 1000L)); ModWorkManagerQueue.replace(); + ManifestCompactor.compactManifests(); } } diff --git a/src/main/java/org/embeddedt/modernfix/forge/classloading/ManifestCompactor.java b/src/main/java/org/embeddedt/modernfix/forge/classloading/ManifestCompactor.java new file mode 100644 index 00000000..24bf22b9 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/classloading/ManifestCompactor.java @@ -0,0 +1,32 @@ +package org.embeddedt.modernfix.forge.classloading; + +import cpw.mods.jarhandling.impl.Jar; +import net.minecraftforge.fml.loading.LoadingModList; + +import java.util.HashSet; +import java.util.Set; +import java.util.jar.Attributes; + +public class ManifestCompactor { + public static void compactManifests() { + for (var mfi : LoadingModList.get().getModFiles()) { + if (!(mfi.getFile().getSecureJar() instanceof Jar jar)) { + continue; + } + var manifest = jar.getManifest(); + if (manifest == null) { + continue; + } + var entries = jar.getManifest().getEntries(); + var entryKeys = new HashSet<>(entries.keySet()); + var digests = Set.of(new Attributes.Name("SHA-256-Digest"), new Attributes.Name("SHA-384-Digest")); + entryKeys.forEach(key -> entries.compute(key, (k, attrs) -> { + if (attrs != null && attrs.keySet().stream().anyMatch(n -> n != null && !digests.contains(n))) { + return attrs; + } else { + return null; + } + })); + } + } +} From f23348c6cbf68bb44f4fdd95f900f7106278cb11 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:28:52 -0500 Subject: [PATCH 32/52] Clear unneeded ObjectHolderRefs --- .../object_holder_cleanup/GameDataMixin.java | 16 +++++++ .../modernfix/forge/init/ModernFixForge.java | 1 + .../forge/registry/ObjectHolderClearer.java | 46 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/object_holder_cleanup/GameDataMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/object_holder_cleanup/GameDataMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/object_holder_cleanup/GameDataMixin.java new file mode 100644 index 00000000..aa9207a1 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/object_holder_cleanup/GameDataMixin.java @@ -0,0 +1,16 @@ +package org.embeddedt.modernfix.common.mixin.perf.object_holder_cleanup; + +import net.minecraftforge.registries.GameData; +import org.embeddedt.modernfix.forge.registry.ObjectHolderClearer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = GameData.class, remap = false) +public class GameDataMixin { + @Inject(method = "postRegisterEvents", at = @At("RETURN")) + private static void clearRedundantHolders(CallbackInfo ci) { + ObjectHolderClearer.removeRedundantHolders(); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java index a95cbdd2..365c0757 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java +++ b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java @@ -125,6 +125,7 @@ public class ModernFixForge { }); } ObjectHolderClearer.clearThrowables(); + event.enqueueWork(ObjectHolderClearer::removeRedundantHolders); } @SubscribeEvent(priority = EventPriority.LOWEST) public void onServerDead(ServerStoppedEvent event) { diff --git a/src/main/java/org/embeddedt/modernfix/forge/registry/ObjectHolderClearer.java b/src/main/java/org/embeddedt/modernfix/forge/registry/ObjectHolderClearer.java index 4f2a9059..c8c4ac24 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/registry/ObjectHolderClearer.java +++ b/src/main/java/org/embeddedt/modernfix/forge/registry/ObjectHolderClearer.java @@ -2,11 +2,14 @@ package org.embeddedt.modernfix.forge.registry; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; +import net.minecraftforge.registries.ForgeRegistry; import net.minecraftforge.registries.ObjectHolderRegistry; import org.embeddedt.modernfix.ModernFix; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; @@ -43,4 +46,47 @@ public class ObjectHolderClearer { ModernFix.LOGGER.debug("Cleared " + numCleared + " object holder stacktrace references"); } } + + public static void removeRedundantHolders() { + try { + Field holdersField = ObjectHolderRegistry.class.getDeclaredField("objectHolders"); + holdersField.setAccessible(true); + Set>> holders = (Set>>)holdersField.get(null); + + Class refClass = Class.forName("net.minecraftforge.registries.ObjectHolderRef"); + Field registryField = refClass.getDeclaredField("registry"); + registryField.setAccessible(true); + Field injectedObjectField = refClass.getDeclaredField("injectedObject"); + injectedObjectField.setAccessible(true); + + Method getOverrideOwnersMethod = ForgeRegistry.class.getDeclaredMethod("getOverrideOwners"); + getOverrideOwnersMethod.setAccessible(true); + + HashMap, Map> overrideCache = new HashMap<>(); + int removed = 0; + + var it = holders.iterator(); + while (it.hasNext()) { + var holder = it.next(); + if (!refClass.isInstance(holder)) + continue; + ForgeRegistry registry = (ForgeRegistry)registryField.get(holder); + ResourceLocation injectedObject = (ResourceLocation)injectedObjectField.get(holder); + Map overrides = overrideCache.computeIfAbsent(registry, r -> { + try { + return (Map)getOverrideOwnersMethod.invoke(r); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + }); + if (!overrides.containsKey(injectedObject)) { + it.remove(); + removed++; + } + } + ModernFix.LOGGER.debug("Removed {} redundant object holders", removed); + } catch (Exception e) { + ModernFix.LOGGER.error("Failed to remove object holders", e); + } + } } From 17f930ea6f1a82a3d1dd5acbedfb5abcad8e6375 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:41:28 -0500 Subject: [PATCH 33/52] WIP chunk saving optimization --- .../ChunkHolderMixin.java | 27 ++++++++++++++ .../ChunkMapAccessor.java | 12 +++++++ .../ChunkSerializerMixin.java | 26 ++++++++++++++ .../ThreadedLevelLightEngineMixin.java | 36 +++++++++++++++++++ .../core/config/ModernFixEarlyConfig.java | 1 + 5 files changed, 102 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkHolderMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkMapAccessor.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkSerializerMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ThreadedLevelLightEngineMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkHolderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkHolderMixin.java new file mode 100644 index 00000000..4baa7b3d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkHolderMixin.java @@ -0,0 +1,27 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.skip_redundant_saves; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +import javax.annotation.Nullable; + +@Mixin(ChunkHolder.class) +public abstract class ChunkHolderMixin { + @Shadow + @Nullable + public abstract LevelChunk getTickingChunk(); + + /** + * @author embeddedt + * @reason prevent chunks from being flagged for saving when light engine is loading data from disk + */ + @WrapWithCondition(method = "sectionLightChanged", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/ChunkAccess;setUnsaved(Z)V")) + private boolean onlyMarkUnsavedIfAlreadyTicking(ChunkAccess instance, boolean unsaved) { + return this.getTickingChunk() != null; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkMapAccessor.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkMapAccessor.java new file mode 100644 index 00000000..649d1c28 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkMapAccessor.java @@ -0,0 +1,12 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.skip_redundant_saves; + +import net.minecraft.server.level.ChunkMap; +import net.minecraft.world.level.ChunkPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(ChunkMap.class) +public interface ChunkMapAccessor { + @Invoker("releaseLightTicket") + void mfix$invokeReleaseLightTicket(ChunkPos pos); +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkSerializerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkSerializerMixin.java new file mode 100644 index 00000000..a1574349 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkSerializerMixin.java @@ -0,0 +1,26 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.skip_redundant_saves; + +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.storage.ChunkSerializer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ChunkSerializer.class) +public class ChunkSerializerMixin { + /** + * @author embeddedt + * @reason When reloading chunks from disk, they by definition normally don't need saving unless they've changed. + */ + @Inject(method = "read", at = @At(value = "CONSTANT", args = "stringValue=PostProcessing", ordinal = 0)) + private static void updateUnsavedFlag(ServerLevel level, PoiManager poiManager, ChunkPos pos, CompoundTag tag, CallbackInfoReturnable cir, @Local(ordinal = 0) ChunkAccess chunkaccess) { + chunkaccess.setUnsaved(tag.getBoolean("shouldSave")); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ThreadedLevelLightEngineMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ThreadedLevelLightEngineMixin.java new file mode 100644 index 00000000..4ffa7362 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ThreadedLevelLightEngineMixin.java @@ -0,0 +1,36 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.skip_redundant_saves; + +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.concurrent.CompletableFuture; + +@Mixin(ThreadedLevelLightEngine.class) +public class ThreadedLevelLightEngineMixin { + @Shadow + @Final + private ChunkMap chunkMap; + + /** + * @author embeddedt + * @reason avoid toggling the lightCorrect flag when chunk is already lit, because it triggers saving + */ + @Inject(method = "lightChunk", at = @At("HEAD"), cancellable = true) + private void skipLightCorrectFlagChange(ChunkAccess chunk, boolean isAlreadyLit, CallbackInfoReturnable> cir) { + if (isAlreadyLit) { + ((ChunkMapAccessor)this.chunkMap).mfix$invokeReleaseLightTicket(chunk.getPos()); + // Defensively ensure the lightCorrect flag is set properly on exit from this method + if (!chunk.isLightCorrect()) { + chunk.setLightCorrect(true); + } + cir.setReturnValue(CompletableFuture.completedFuture(chunk)); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index 094cfa99..1b31f3b5 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -184,6 +184,7 @@ public class ModernFixEarlyConfig { .put("mixin.feature.spam_thread_dump", false) .put("mixin.feature.disable_unihex_font", false) .put("mixin.feature.remove_chat_signing", false) + .put("mixin.bugfix.skip_redundant_saves", false) .put("mixin.feature.snapshot_easter_egg", true) .put("mixin.feature.warn_missing_perf_mods", true) .put("mixin.feature.spark_profile_launch", false) From da2206168bfcc7808ae110d441007f38c17ecf39 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:18:01 -0500 Subject: [PATCH 34/52] Port AP to Java 17 --- annotation-processor/build.gradle | 2 +- .../annotation/ClientMixinValidator.java | 27 ++++++++----------- build.gradle.kts | 5 ---- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/annotation-processor/build.gradle b/annotation-processor/build.gradle index beb7ea1b..2921bf3d 100644 --- a/annotation-processor/build.gradle +++ b/annotation-processor/build.gradle @@ -30,7 +30,7 @@ dependencies { } tasks.withType(JavaCompile) { - options.release = 21 + options.release = 17 } shadowJar { diff --git a/annotation-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/ClientMixinValidator.java b/annotation-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/ClientMixinValidator.java index 139c6b20..df0fab31 100644 --- a/annotation-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/ClientMixinValidator.java +++ b/annotation-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/ClientMixinValidator.java @@ -90,24 +90,19 @@ public class ClientMixinValidator { } private boolean targetsClient(Object classTarget) { - return switch (classTarget) { - case TypeElement te -> - isClientMarked(te); - case TypeMirror tm -> { - var el = types.asElement(tm); - yield el != null ? targetsClient(el) : warn("TypeMirror of " + tm); - } - // If you're using a dollar sign in class names you are insane - case String s -> { - var te = - elemUtils.getTypeElement(toSourceString(s.split("\\$")[0])); - yield te != null ? targetsClient(te) : warn(s); - } - default -> - throw new IllegalArgumentException("Unhandled type: " + if (classTarget instanceof TypeElement te) { + return isClientMarked(te); + } else if (classTarget instanceof TypeMirror tm) { + var el = types.asElement(tm); + return el != null ? targetsClient(el) : warn("TypeMirror of " + tm); + } else if (classTarget instanceof String s) { + var te = elemUtils.getTypeElement(toSourceString(s.split("\\$")[0])); + return te != null ? targetsClient(te) : warn(s); + } else { + throw new IllegalArgumentException("Unhandled type: " + classTarget.getClass() + "\n" + "Stringified contents: " + classTarget.toString()); - }; + } } private boolean isClientMarked(TypeElement te) { diff --git a/build.gradle.kts b/build.gradle.kts index 29f385ef..25f1716e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -91,12 +91,7 @@ tasks.named("jar") { )) } -// We must force the Java 21 compiler to be used because our AP requires Java 21 - java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } val curSourceCompatLevel = JavaVersion.VERSION_17 sourceCompatibility = curSourceCompatLevel targetCompatibility = curSourceCompatLevel From bee4536c1aa9f1f566d680c3f3642b599740dfbe Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:09:33 -0500 Subject: [PATCH 35/52] Tweak full chunk promotion to reduce opportunities for deadlocks --- .../chunk_deadlock/ChunkMapLoadMixin.java | 84 ++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java index 0a174b76..4986358b 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java @@ -2,21 +2,101 @@ package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.datafixers.util.Either; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; import org.embeddedt.modernfix.ModernFix; import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.lang.reflect.Field; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; @Mixin(ChunkMap.class) public abstract class ChunkMapLoadMixin { - @Shadow @Nullable protected abstract ChunkHolder getVisibleChunkIfPresent(long l); + @Shadow + @Nullable + protected abstract ChunkHolder getVisibleChunkIfPresent(long l); + + @Shadow + @Final + private BlockableEventLoop mainThreadExecutor; + + @Unique + private static final ThreadLocal>> MFIX_SURROGATE_FUTURE = new ThreadLocal<>(); + + /** + * @author embeddedt + * @reason This redirect makes several changes to how full chunk promotion works. First of all, promotion runs + * directly in the context of the main thread executor, rather than going through the priority sorter. + * This change allows attempts to load other chunks from within the promotion lambda to succeed (important + * for bad EntityJoinLevelEvent implementations to not deadlock the game). Second, it slightly alters the + * semantics of protoChunkToFullChunk so that the FULL chunk future will be completed before postload + * callbacks finish running. This change allows attempts to load the _same_ chunk in the promotion lambda to + * succeed, as otherwise the future would block waiting for itself to complete. + * + *

This is a cleaner version of a similar trick used in ModernFix versions for 1.16, which deferred specifically + * entity addition to happen outside the futures. + */ + @Redirect(method = "protoChunkToFullChunk", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;", ordinal = 0)) + private CompletableFuture> createSurrogateFuture( + CompletableFuture> previousFuture, + Function, ? extends Either> fn, + Executor executor) { + var surrogate = new CompletableFuture>(); + // Unlike vanilla, we execute the promotion lambda in mainThreadExecutor, rather than within the context + // of the task sorter. Doing this avoids deadlocking the sorter if a blocking chunk load is attempted + // during chunk promotion. We still initially compose the future through the sorter's executor to stop promotion + // from running earlier than it would in vanilla. + previousFuture.thenComposeAsync(CompletableFuture::completedFuture, executor).thenApplyAsync(either -> { + // running on thread that executes lambda body + MFIX_SURROGATE_FUTURE.set(surrogate); + try { + return fn.apply(either); + } finally { + MFIX_SURROGATE_FUTURE.remove(); + } + }, this.mainThreadExecutor).whenComplete((either, throwable) -> { + if (throwable != null) { + surrogate.completeExceptionally(throwable); + } else { + surrogate.complete(either); + } + }); + // Return the surrogate + return surrogate; + } + + /** + * @author embeddedt + * @reason Complete the surrogate future as soon as basic promotion is done, and before we start loading entities + * & block entities. This allows EntityJoinLevelEvent to read the current chunk. + */ + @Inject(method = "lambda$protoChunkToFullChunk$34", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V")) + private void completeSurrogateFuture(ChunkHolder holder, ChunkAccess p_214856_, CallbackInfoReturnable cir, + @Local(ordinal = 0) LevelChunk levelChunk) { + var future = MFIX_SURROGATE_FUTURE.get(); + if (future != null) { + future.complete(Either.left(levelChunk)); + } + } + + + // we also preserve the legacy currentlyLoading field to keep Forge parity private static final Field currentlyLoadingField = ObfuscationReflectionHelper.findField(ChunkHolder.class, "currentlyLoading"); @@ -32,7 +112,7 @@ public abstract class ChunkMapLoadMixin { * Set currentlyLoading before calling runPostLoad and restore its old value afterwards. We track the old value * to avoid conflicting with Forge if/when this feature is added. */ - @WrapOperation(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V")) + @WrapOperation(method = "lambda$protoChunkToFullChunk$34", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V")) private void setCurrentLoadingThenPostLoad(LevelChunk chunk, Operation operation) { ChunkHolder holder = this.getVisibleChunkIfPresent(chunk.getPos().toLong()); if(holder != null) { From ac8d93d5b95a90c92f95b11fe46fc49beb565aae Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:00:28 -0500 Subject: [PATCH 36/52] Ensure exceptions thrown in chunk load events are not dropped --- .../chunk_deadlock/ChunkMapLoadMixin.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java index 4986358b..0cacb879 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java @@ -4,6 +4,8 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; import com.mojang.datafixers.util.Either; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; import net.minecraft.util.thread.BlockableEventLoop; @@ -19,10 +21,12 @@ import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.lang.reflect.Field; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; import java.util.function.Function; @@ -39,6 +43,9 @@ public abstract class ChunkMapLoadMixin { @Unique private static final ThreadLocal>> MFIX_SURROGATE_FUTURE = new ThreadLocal<>(); + @Unique + private final ConcurrentLinkedQueue mfix$promotionExceptions = new ConcurrentLinkedQueue<>(); + /** * @author embeddedt * @reason This redirect makes several changes to how full chunk promotion works. First of all, promotion runs @@ -72,7 +79,14 @@ public abstract class ChunkMapLoadMixin { } }, this.mainThreadExecutor).whenComplete((either, throwable) -> { if (throwable != null) { - surrogate.completeExceptionally(throwable); + if (!surrogate.isDone()) { + surrogate.completeExceptionally(throwable); + } else { + // The chunk has already become visible at FULL status, so we + // track the exception ourselves and manually rethrow it at the right point + // to trigger a server crash + this.mfix$promotionExceptions.add(throwable); + } } else { surrogate.complete(either); } @@ -95,6 +109,19 @@ public abstract class ChunkMapLoadMixin { } } + @Inject(method = "tick()V", at = @At("HEAD")) + private void reportDeferredPromotionException(CallbackInfo ci) { + var throwable = this.mfix$promotionExceptions.poll(); + if (throwable == null) { + return; + } + if (throwable instanceof ReportedException e) { + throw e; + } else { + throw new ReportedException(CrashReport.forThrowable(throwable, "Exception during promotion of chunk to FULL status")); + } + } + // we also preserve the legacy currentlyLoading field to keep Forge parity From 9edce9ad91ae833219a2a33b839fb66ca19c6bad Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:52:26 -0500 Subject: [PATCH 37/52] Dynamically load/unload Unihex font data --- .../GlyphProviderTypeMixin.java | 20 ++++ .../render/font/LazyGlyphProvider.java | 105 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/GlyphProviderTypeMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/GlyphProviderTypeMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/GlyphProviderTypeMixin.java new file mode 100644 index 00000000..bdb2db07 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/GlyphProviderTypeMixin.java @@ -0,0 +1,20 @@ +package org.embeddedt.modernfix.common.mixin.perf.compress_unihex_font; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.mojang.serialization.MapCodec; +import net.minecraft.client.gui.font.providers.GlyphProviderDefinition; +import net.minecraft.client.gui.font.providers.GlyphProviderType; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.render.font.LazyGlyphProvider; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GlyphProviderType.class) +@ClientOnlyMixin +public class GlyphProviderTypeMixin { + @ModifyExpressionValue(method = "", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/minecraft/client/gui/font/providers/UnihexProvider$Definition;CODEC:Lcom/mojang/serialization/MapCodec;")) + private static MapCodec lazyUnihex(MapCodec codec) { + return LazyGlyphProvider.wrap(codec); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java b/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java new file mode 100644 index 00000000..74e49d6f --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java @@ -0,0 +1,105 @@ +package org.embeddedt.modernfix.render.font; + +import com.mojang.blaze3d.font.GlyphInfo; +import com.mojang.blaze3d.font.GlyphProvider; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.MapCodec; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.client.gui.font.providers.GlyphProviderDefinition; +import net.minecraft.client.gui.font.providers.GlyphProviderType; +import net.minecraft.server.packs.resources.ResourceManager; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.function.Function; + +public class LazyGlyphProvider implements GlyphProvider { + private final GlyphProviderDefinition.Loader loader; + private final ResourceManager manager; + + private SoftReference innerProvider = new SoftReference<>(null); + + private IntSet supportedGlyphs; + + LazyGlyphProvider(GlyphProviderDefinition.Loader loader, ResourceManager manager) { + this.loader = loader; + this.manager = manager; + } + + @Override + public void close() { + // best effort + var prov = innerProvider.get(); + if (prov != null) { + prov.close(); + } + } + + private @Nullable GlyphProvider getGlyphProvider() { + GlyphProvider prov = innerProvider.get(); + if (prov == null) { + try { + prov = this.loader.load(this.manager); + } catch (IOException e) { + return null; + } + innerProvider = new SoftReference<>(prov); + } + return prov; + } + + @Override + public @Nullable GlyphInfo getGlyph(int character) { + var prov = getGlyphProvider(); + if (prov != null) { + return prov.getGlyph(character); + } else { + return null; + } + } + + @Override + public IntSet getSupportedGlyphs() { + if (supportedGlyphs == null) { + var prov = getGlyphProvider(); + if (prov != null) { + supportedGlyphs = new IntOpenHashSet(prov.getSupportedGlyphs()); + } else { + supportedGlyphs = IntSet.of(); + } + } + return supportedGlyphs; + } + + private static class Definition implements GlyphProviderDefinition { + private final GlyphProviderDefinition delegate; + + public Definition(GlyphProviderDefinition delegate) { + this.delegate = delegate; + } + + @Override + public GlyphProviderType type() { + return this.delegate.type(); + } + + @Override + public Either unpack() { + return this.delegate.unpack().mapBoth( + loader -> resourceManager -> new LazyGlyphProvider(loader, resourceManager), + Function.identity() + ); + } + + @SuppressWarnings("unchecked") + public T delegate() { + return (T)this.delegate; + } + } + + public static MapCodec wrap(MapCodec codec) { + return codec.xmap(Definition::new, Definition::delegate); + } +} From 02f486ebf4aa8f887887f5fa4df89b8d22e7d500 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:36:15 -0400 Subject: [PATCH 38/52] Avoid loading multiple copies of a lazy glyph provider --- .../embeddedt/modernfix/render/font/LazyGlyphProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java b/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java index 74e49d6f..c8dedbcc 100644 --- a/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java +++ b/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java @@ -37,7 +37,7 @@ public class LazyGlyphProvider implements GlyphProvider { } } - private @Nullable GlyphProvider getGlyphProvider() { + private synchronized @Nullable GlyphProvider getGlyphProvider() { GlyphProvider prov = innerProvider.get(); if (prov == null) { try { @@ -61,7 +61,7 @@ public class LazyGlyphProvider implements GlyphProvider { } @Override - public IntSet getSupportedGlyphs() { + public synchronized IntSet getSupportedGlyphs() { if (supportedGlyphs == null) { var prov = getGlyphProvider(); if (prov != null) { From 2050516bf193164011b41f670640579ed61b0929 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:53:33 -0400 Subject: [PATCH 39/52] Do not cache supported glyphs in lazy provider --- .../render/font/LazyGlyphProvider.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java b/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java index c8dedbcc..d860cfa3 100644 --- a/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java +++ b/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java @@ -4,7 +4,6 @@ import com.mojang.blaze3d.font.GlyphInfo; import com.mojang.blaze3d.font.GlyphProvider; import com.mojang.datafixers.util.Either; import com.mojang.serialization.MapCodec; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import net.minecraft.client.gui.font.providers.GlyphProviderDefinition; import net.minecraft.client.gui.font.providers.GlyphProviderType; @@ -21,8 +20,6 @@ public class LazyGlyphProvider implements GlyphProvider { private SoftReference innerProvider = new SoftReference<>(null); - private IntSet supportedGlyphs; - LazyGlyphProvider(GlyphProviderDefinition.Loader loader, ResourceManager manager) { this.loader = loader; this.manager = manager; @@ -61,16 +58,13 @@ public class LazyGlyphProvider implements GlyphProvider { } @Override - public synchronized IntSet getSupportedGlyphs() { - if (supportedGlyphs == null) { - var prov = getGlyphProvider(); - if (prov != null) { - supportedGlyphs = new IntOpenHashSet(prov.getSupportedGlyphs()); - } else { - supportedGlyphs = IntSet.of(); - } + public IntSet getSupportedGlyphs() { + var prov = getGlyphProvider(); + if (prov != null) { + return prov.getSupportedGlyphs(); + } else { + return IntSet.of(); } - return supportedGlyphs; } private static class Definition implements GlyphProviderDefinition { From 38288d5e6a6547e00767203bba9096ff8f365c50 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:26:51 -0400 Subject: [PATCH 40/52] Automatically free contents of ChunkHolders only used for worldgen when generation finishes --- .../release_protochunks/ChunkHolderMixin.java | 97 +++++++++++ .../release_protochunks/ChunkMapMixin.java | 153 ++++++++++++++++++ .../ThreadedLevelLightEngineAccessor.java | 12 ++ .../IClearableChunkHolder.java | 9 ++ .../ISuspendedHolderTrackingChunkMap.java | 11 ++ 5 files changed, 282 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ThreadedLevelLightEngineAccessor.java create mode 100644 src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java create mode 100644 src/main/java/org/embeddedt/modernfix/duck/release_protochunks/ISuspendedHolderTrackingChunkMap.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java new file mode 100644 index 00000000..b66ff4dc --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java @@ -0,0 +1,97 @@ +package org.embeddedt.modernfix.common.mixin.perf.release_protochunks; + +import com.mojang.datafixers.util.Either; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder; +import org.embeddedt.modernfix.duck.release_protochunks.ISuspendedHolderTrackingChunkMap; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +@Mixin(ChunkHolder.class) +public class ChunkHolderMixin implements IClearableChunkHolder { + @Shadow + @Final + private AtomicReferenceArray>> futures; + + @Shadow + private CompletableFuture chunkToSave; + + @Shadow + private int ticketLevel; + + @Shadow + @Final + private ChunkPos pos; + + @Shadow + @Final + private ChunkHolder.PlayerProvider playerProvider; + + /** + * Used to track the number of neighboring holders actively using this chunk for generation. + */ + @Unique + private final AtomicInteger mfix$generationRefCount = new AtomicInteger(0); + + @Override + public void mfix$resetProtoChunkFutures() { + int len = this.futures.length(); + for (int i = 0; i < len; i++) { + this.futures.set(i, null); + } + this.chunkToSave = CompletableFuture.completedFuture(null); + } + + @Override + public AtomicInteger mfix$getGenerationRefCount() { + return this.mfix$generationRefCount; + } + + /* + * The methods below trigger the ChunkMap to check whether this holder can be "suspended" (have its ProtoChunk-only + * futures cleared) each time a new version of the chunkToSave future has completed. The ChunkMap itself + * also verifies that all conditions are still met for suspension in case the holder has become necessary + * again in the meantime. + */ + + @Inject(method = "addSaveDependency", at = @At("RETURN")) + private void recheckSuspensionAfterNeighbor(String source, CompletableFuture future, CallbackInfo ci) { + this.mfix$markAsNeedingProtoChunkDrop(); + } + + @Inject(method = "updateChunkToSave", at = @At("RETURN")) + private void checkSuspension(CallbackInfo ci) { + this.mfix$markAsNeedingProtoChunkDrop(); + } + + @Inject(method = "updateFutures", at = @At("RETURN")) + private void markForSuspensionOnDemotion(ChunkMap chunkMap, Executor executor, CallbackInfo ci) { + this.mfix$markAsNeedingProtoChunkDrop(); + } + + private void mfix$markAsNeedingProtoChunkDrop() { + if (!ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL) + && ChunkLevel.isLoaded(this.ticketLevel)) { + // register for suspension check when chain completes + var map = ((ISuspendedHolderTrackingChunkMap)this.playerProvider); + this.chunkToSave.whenCompleteAsync((r, e) -> { + map.mfix$markForSuspensionCheck(this.pos); + }, map.mfix$getMainThreadExecutor()); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java new file mode 100644 index 00000000..959da87c --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java @@ -0,0 +1,153 @@ +package org.embeddedt.modernfix.common.mixin.perf.release_protochunks; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.datafixers.util.Either; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder; +import org.embeddedt.modernfix.duck.release_protochunks.ISuspendedHolderTrackingChunkMap; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; + +@Mixin(ChunkMap.class) +public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap { + @Shadow + @Final + public Long2ObjectLinkedOpenHashMap updatingChunkMap; + + @Shadow + protected abstract boolean save(ChunkAccess chunk); + + @Shadow + @Final + private ChunkProgressListener progressListener; + @Shadow + @Final + private ThreadedLevelLightEngine lightEngine; + + @Shadow + @Final + private BlockableEventLoop mainThreadExecutor; + + private final LongOpenHashSet mfix$protoChunksToDrop = new LongOpenHashSet(); + + /** + * @author embeddedt + * @reason We keep track of ChunkHolders that only contain protochunks, and are not loaded to a full status. + * This hook unloads their contents once there are no generation tasks actively relying on the chunk. + */ + @Inject(method = "processUnloads(Ljava/util/function/BooleanSupplier;)V", at = @At("RETURN")) + private void dropProtoChunks(BooleanSupplier hasMoreTime, CallbackInfo ci) { + int suspended = 0; + int iterations = 0; + LongIterator dropIterator = mfix$protoChunksToDrop.longIterator(); + while (dropIterator.hasNext() && suspended < 50 && iterations < 500 && (hasMoreTime.getAsBoolean() || mfix$protoChunksToDrop.size() > 1000)) { + iterations++; + long pos = dropIterator.nextLong(); + ChunkHolder holder = this.updatingChunkMap.get(pos); + if (holder == null // already removed + || ChunkLevel.fullStatus(holder.getTicketLevel()).isOrAfter(FullChunkStatus.FULL) // promoted to FULL + || !ChunkLevel.isLoaded(holder.getTicketLevel()) // is going to be dropped through normal code path + ) { + dropIterator.remove(); + continue; + } + + if (!holder.getChunkToSave().isDone() + || ((IClearableChunkHolder)holder).mfix$getGenerationRefCount().get() != 0) { + // Not safe to suspend yet; either the chunkToSave chain is still pending, or a neighbor's + // generation task is still actively using this chunk's sections + continue; + } + + // All generation work done, so we can suspend and remove from set + dropIterator.remove(); + + ChunkAccess chunk = holder.getChunkToSave().getNow(null); + if (chunk != null) { + this.save(chunk); // flush protochunk to disk + } + + ((IClearableChunkHolder)holder).mfix$resetProtoChunkFutures(); + + this.progressListener.onStatusChange(holder.getPos(), null); + ((ThreadedLevelLightEngineAccessor)this.lightEngine).mfix$invokeUpdateChunkStatus(holder.getPos()); + this.lightEngine.tryScheduleUpdate(); + suspended++; + } + } + + /** + * @author embeddedt + * @reason increment the generation ref count on all neighboring chunk holders within the range when a generation + * task starts + */ + @Inject(method = "scheduleChunkGeneration", at = @At("HEAD")) + private void incrementGenRefCounts(ChunkHolder chunkHolder, ChunkStatus chunkStatus, CallbackInfoReturnable>> cir) { + int range = chunkStatus.getRange(); + ChunkPos center = chunkHolder.getPos(); + for (int dx = -range; dx <= range; dx++) { + for (int dz = -range; dz <= range; dz++) { + ChunkHolder neighbor = this.updatingChunkMap.get(ChunkPos.asLong(center.x + dx, center.z + dz)); + if (neighbor != null) { + ((IClearableChunkHolder)neighbor).mfix$getGenerationRefCount().incrementAndGet(); + } + } + } + } + + /** + * @author embeddedt + * @reason decrement the generation ref count on all neighboring chunk holders within the range when the generation + * task is completely finished + */ + @ModifyReturnValue(method = "scheduleChunkGeneration", at = @At("RETURN")) + private CompletableFuture> decrementGenRefCountsOnComplete(CompletableFuture> future, + @Local(ordinal = 0, argsOnly = true) ChunkHolder chunkHolder, + @Local(ordinal = 0, argsOnly = true) ChunkStatus chunkStatus) { + int range = chunkStatus.getRange(); + ChunkPos center = chunkHolder.getPos(); + return future.whenCompleteAsync((result, error) -> { + for (int dx = -range; dx <= range; dx++) { + for (int dz = -range; dz <= range; dz++) { + ChunkHolder neighbor = this.updatingChunkMap.get(ChunkPos.asLong(center.x + dx, center.z + dz)); + if (neighbor != null) { + ((IClearableChunkHolder)neighbor).mfix$getGenerationRefCount().decrementAndGet(); + } + } + } + }, this.mainThreadExecutor); + } + + @Override + public void mfix$markForSuspensionCheck(ChunkPos pos) { + this.mfix$protoChunksToDrop.add(pos.toLong()); + } + + @Override + public Executor mfix$getMainThreadExecutor() { + return this.mainThreadExecutor; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ThreadedLevelLightEngineAccessor.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ThreadedLevelLightEngineAccessor.java new file mode 100644 index 00000000..cbebcb6a --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ThreadedLevelLightEngineAccessor.java @@ -0,0 +1,12 @@ +package org.embeddedt.modernfix.common.mixin.perf.release_protochunks; + +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.world.level.ChunkPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(ThreadedLevelLightEngine.class) +public interface ThreadedLevelLightEngineAccessor { + @Invoker("updateChunkStatus") + void mfix$invokeUpdateChunkStatus(ChunkPos pos); +} diff --git a/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java new file mode 100644 index 00000000..b6910cf6 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java @@ -0,0 +1,9 @@ +package org.embeddedt.modernfix.duck.release_protochunks; + +import java.util.concurrent.atomic.AtomicInteger; + +public interface IClearableChunkHolder { + void mfix$resetProtoChunkFutures(); + + AtomicInteger mfix$getGenerationRefCount(); +} diff --git a/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/ISuspendedHolderTrackingChunkMap.java b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/ISuspendedHolderTrackingChunkMap.java new file mode 100644 index 00000000..2fb171fb --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/ISuspendedHolderTrackingChunkMap.java @@ -0,0 +1,11 @@ +package org.embeddedt.modernfix.duck.release_protochunks; + +import net.minecraft.world.level.ChunkPos; + +import java.util.concurrent.Executor; + +public interface ISuspendedHolderTrackingChunkMap { + void mfix$markForSuspensionCheck(ChunkPos pos); + + Executor mfix$getMainThreadExecutor(); +} From f79eae8b8384de77e66a060e1c27c797f3bda049 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 14 Mar 2026 10:44:04 -0400 Subject: [PATCH 41/52] Make integrated server treat game as paused while singleplayer client is still loading --- .../ClientPacketListenerMixin.java | 36 +++++++++++++++++++ .../IntegratedServerMixin.java | 33 +++++++++++++++++ .../PlayerListMixin.java | 26 ++++++++++++++ .../IDeferrableIntegratedServer.java | 10 ++++++ 4 files changed, 105 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/ClientPacketListenerMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/PlayerListMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/duck/suspend_integrated_server_during_load/IDeferrableIntegratedServer.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/ClientPacketListenerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/ClientPacketListenerMixin.java new file mode 100644 index 00000000..2e2d6b53 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/ClientPacketListenerMixin.java @@ -0,0 +1,36 @@ +package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPacketListener.class) +@ClientOnlyMixin +public class ClientPacketListenerMixin { + @Shadow + @Final + private Minecraft minecraft; + + @Inject(method = "handleCustomPayload", at = @At("HEAD"), cancellable = true) + private void detectClientLoadSentinel(ClientboundCustomPayloadPacket packet, CallbackInfo ci) { + if (packet.getIdentifier().equals(IDeferrableIntegratedServer.CLIENT_LOAD_SENTINEL)) { + // Important: flag must be changed on the client thread, as later packets can start decoding + // while earlier ones are still being applied. + this.minecraft.executeIfPossible(() -> { + packet.getData().release(); + if (this.minecraft.hasSingleplayerServer()) { + ((IDeferrableIntegratedServer)this.minecraft.getSingleplayerServer()).mfix$markClientLoadFinished(); + } + }); + ci.cancel(); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java new file mode 100644 index 00000000..00791452 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java @@ -0,0 +1,33 @@ +package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.client.Minecraft; +import net.minecraft.client.server.IntegratedServer; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.concurrent.atomic.AtomicBoolean; + +@Mixin(IntegratedServer.class) +@ClientOnlyMixin +public abstract class IntegratedServerMixin implements IDeferrableIntegratedServer { + private final AtomicBoolean mfix$hasPrimaryClientJoined = new AtomicBoolean(false); + + /** + * @author embeddedt + * @reason Wait to be finished processing all expensive packets (recipes, tags, etc.) + * before continuing to tick the integrated server. + */ + @WrapOperation(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;isPaused()Z", ordinal = 0)) + private boolean preventTicks(Minecraft instance, Operation original) { + return !mfix$hasPrimaryClientJoined.get() || original.call(instance); + } + + @Override + public void mfix$markClientLoadFinished() { + mfix$hasPrimaryClientJoined.set(true); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/PlayerListMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/PlayerListMixin.java new file mode 100644 index 00000000..ad802156 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/PlayerListMixin.java @@ -0,0 +1,26 @@ +package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerList.class) +@ClientOnlyMixin +public class PlayerListMixin { + @Inject(method = "placeNewPlayer", at = @At("RETURN")) + private void sendConfigFinishedSentinelPacket(Connection connection, ServerPlayer player, CallbackInfo ci) { + if (connection.isMemoryConnection()) { + FriendlyByteBuf friendlybytebuf = new FriendlyByteBuf(Unpooled.buffer()); + player.connection.send(new ClientboundCustomPayloadPacket(IDeferrableIntegratedServer.CLIENT_LOAD_SENTINEL, friendlybytebuf)); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/duck/suspend_integrated_server_during_load/IDeferrableIntegratedServer.java b/src/main/java/org/embeddedt/modernfix/duck/suspend_integrated_server_during_load/IDeferrableIntegratedServer.java new file mode 100644 index 00000000..452fe0fa --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/suspend_integrated_server_during_load/IDeferrableIntegratedServer.java @@ -0,0 +1,10 @@ +package org.embeddedt.modernfix.duck.suspend_integrated_server_during_load; + +import net.minecraft.resources.ResourceLocation; +import org.embeddedt.modernfix.ModernFix; + +public interface IDeferrableIntegratedServer { + ResourceLocation CLIENT_LOAD_SENTINEL = new ResourceLocation(ModernFix.MODID, "mark_client_load_finished"); + + void mfix$markClientLoadFinished(); +} From e34a99b38c0c61b0d78fc7c9d1499a06201a0daa Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:59:45 -0400 Subject: [PATCH 42/52] Simplify chunk unload logic & fix events not being fired when INACCESSIBLE chunks are unloaded --- .../release_protochunks/ChunkMapMixin.java | 32 +++++++------------ .../ThreadedLevelLightEngineAccessor.java | 12 ------- 2 files changed, 12 insertions(+), 32 deletions(-) delete mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ThreadedLevelLightEngineAccessor.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java index 959da87c..abbe5e73 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java @@ -10,8 +10,6 @@ import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkLevel; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.FullChunkStatus; -import net.minecraft.server.level.ThreadedLevelLightEngine; -import net.minecraft.server.level.progress.ChunkProgressListener; import net.minecraft.util.thread.BlockableEventLoop; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.chunk.ChunkAccess; @@ -27,7 +25,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; import java.util.function.BooleanSupplier; @@ -37,20 +34,16 @@ public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap @Final public Long2ObjectLinkedOpenHashMap updatingChunkMap; - @Shadow - protected abstract boolean save(ChunkAccess chunk); - - @Shadow - @Final - private ChunkProgressListener progressListener; - @Shadow - @Final - private ThreadedLevelLightEngine lightEngine; - @Shadow @Final private BlockableEventLoop mainThreadExecutor; + @Shadow + protected abstract void lambda$scheduleUnload$14(ChunkHolder holder, CompletableFuture chunkToSaveFuture, long chunkPos, ChunkAccess chunk); + + @Shadow + @Final + public Long2ObjectLinkedOpenHashMap pendingUnloads; private final LongOpenHashSet mfix$protoChunksToDrop = new LongOpenHashSet(); /** @@ -85,16 +78,15 @@ public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap // All generation work done, so we can suspend and remove from set dropIterator.remove(); - ChunkAccess chunk = holder.getChunkToSave().getNow(null); - if (chunk != null) { - this.save(chunk); // flush protochunk to disk - } + var chunkToSaveFuture = holder.getChunkToSave(); + + // Execute the logic inside scheduleUnload() inline, without delegating to a queue + // When this returns it is safe to release any data the ChunkHolder holds + this.pendingUnloads.put(pos, holder); + this.lambda$scheduleUnload$14(holder, chunkToSaveFuture, pos, chunkToSaveFuture.getNow(null)); ((IClearableChunkHolder)holder).mfix$resetProtoChunkFutures(); - this.progressListener.onStatusChange(holder.getPos(), null); - ((ThreadedLevelLightEngineAccessor)this.lightEngine).mfix$invokeUpdateChunkStatus(holder.getPos()); - this.lightEngine.tryScheduleUpdate(); suspended++; } } diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ThreadedLevelLightEngineAccessor.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ThreadedLevelLightEngineAccessor.java deleted file mode 100644 index cbebcb6a..00000000 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ThreadedLevelLightEngineAccessor.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.perf.release_protochunks; - -import net.minecraft.server.level.ThreadedLevelLightEngine; -import net.minecraft.world.level.ChunkPos; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Invoker; - -@Mixin(ThreadedLevelLightEngine.class) -public interface ThreadedLevelLightEngineAccessor { - @Invoker("updateChunkStatus") - void mfix$invokeUpdateChunkStatus(ChunkPos pos); -} From 9692da12b420b1e186375f49fee0324ff5f835ce Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 14 Mar 2026 15:59:52 -0400 Subject: [PATCH 43/52] Add idle timer to prevent chunks from suspending too quickly --- .../release_protochunks/ChunkMapMixin.java | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java index abbe5e73..916f7325 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java @@ -3,9 +3,8 @@ package org.embeddedt.modernfix.common.mixin.perf.release_protochunks; import com.llamalad7.mixinextras.injector.ModifyReturnValue; import com.llamalad7.mixinextras.sugar.Local; import com.mojang.datafixers.util.Either; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; -import it.unimi.dsi.fastutil.longs.LongIterator; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkLevel; import net.minecraft.server.level.ChunkMap; @@ -30,6 +29,9 @@ import java.util.function.BooleanSupplier; @Mixin(ChunkMap.class) public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap { + + private static final int MFIX$TICKS_TO_WAIT_BEFORE_SUSPENDING = 100; + @Shadow @Final public Long2ObjectLinkedOpenHashMap updatingChunkMap; @@ -44,7 +46,10 @@ public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap @Shadow @Final public Long2ObjectLinkedOpenHashMap pendingUnloads; - private final LongOpenHashSet mfix$protoChunksToDrop = new LongOpenHashSet(); + + private final Long2IntOpenHashMap mfix$protoChunksToDrop = new Long2IntOpenHashMap(); + + private int mfix$dropTickCounter = 0; /** * @author embeddedt @@ -55,10 +60,12 @@ public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap private void dropProtoChunks(BooleanSupplier hasMoreTime, CallbackInfo ci) { int suspended = 0; int iterations = 0; - LongIterator dropIterator = mfix$protoChunksToDrop.longIterator(); + mfix$dropTickCounter++; + var dropIterator = mfix$protoChunksToDrop.long2IntEntrySet().fastIterator(); while (dropIterator.hasNext() && suspended < 50 && iterations < 500 && (hasMoreTime.getAsBoolean() || mfix$protoChunksToDrop.size() > 1000)) { iterations++; - long pos = dropIterator.nextLong(); + var entry = dropIterator.next(); + long pos = entry.getLongKey(); ChunkHolder holder = this.updatingChunkMap.get(pos); if (holder == null // already removed || ChunkLevel.fullStatus(holder.getTicketLevel()).isOrAfter(FullChunkStatus.FULL) // promoted to FULL @@ -68,10 +75,16 @@ public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap continue; } - if (!holder.getChunkToSave().isDone() - || ((IClearableChunkHolder)holder).mfix$getGenerationRefCount().get() != 0) { - // Not safe to suspend yet; either the chunkToSave chain is still pending, or a neighbor's - // generation task is still actively using this chunk's sections + if (!holder.getChunkToSave().isDone() // chunkToSave dependencies have not completed + || ((IClearableChunkHolder)holder).mfix$getGenerationRefCount().get() != 0 // chunk is still being referenced by another chunk for generation + ) { + // Not safe to suspend yet, reset timer + entry.setValue(mfix$dropTickCounter); + continue; + } + + if ((mfix$dropTickCounter - entry.getIntValue()) < MFIX$TICKS_TO_WAIT_BEFORE_SUSPENDING) { + // Chunk has not been idle for long enough, wait continue; } @@ -135,7 +148,7 @@ public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap @Override public void mfix$markForSuspensionCheck(ChunkPos pos) { - this.mfix$protoChunksToDrop.add(pos.toLong()); + this.mfix$protoChunksToDrop.put(pos.toLong(), this.mfix$dropTickCounter); } @Override From 1289897004ab0318f3d37c0d546a8eb644398237 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:33:30 -0400 Subject: [PATCH 44/52] Add worldgen benchmarking harness --- .../benchmark/WorldgenBenchmark.java | 185 ++++++++++++++++++ .../modernfix/forge/init/ModernFixForge.java | 10 + 2 files changed, 195 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/benchmark/WorldgenBenchmark.java diff --git a/src/main/java/org/embeddedt/modernfix/benchmark/WorldgenBenchmark.java b/src/main/java/org/embeddedt/modernfix/benchmark/WorldgenBenchmark.java new file mode 100644 index 00000000..4bd241d7 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/benchmark/WorldgenBenchmark.java @@ -0,0 +1,185 @@ +package org.embeddedt.modernfix.benchmark; + +import com.google.common.util.concurrent.MoreExecutors; +import com.mojang.datafixers.util.Either; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.*; +import net.minecraft.util.Unit; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.chunk.*; +import net.minecraft.world.level.chunk.storage.ChunkSerializer; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.embeddedt.modernfix.ModernFix; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; + +public class WorldgenBenchmark { + + private static final TicketType BENCHMARK_TICKET = + TicketType.create("modernfix_benchmark", (a, b) -> 0); + + private static final List ALL_STATUSES = ChunkStatus.getStatusList().stream() + .filter(s -> s.getIndex() > ChunkStatus.EMPTY.getIndex() + && s.getIndex() < ChunkStatus.INITIALIZE_LIGHT.getIndex()) + .toList(); + + private static final int REQUIRED_LOAD_RADIUS = ALL_STATUSES.stream().mapToInt(ChunkStatus::getRange).max().orElse(0); + + public static String run(ServerLevel level, ChunkPos center, int testRadius, int iterations, ChunkStatus startStatus, ChunkStatus stopStatus) { + int startIndex = ALL_STATUSES.indexOf(startStatus); + if (startIndex < 0) { + throw new IllegalArgumentException("Invalid start status: " + startStatus); + } + + int stopIndex = ALL_STATUSES.indexOf(stopStatus); + if (stopIndex < 0) { + throw new IllegalArgumentException("Invalid stop status:" + stopStatus); + } + + List setupStatuses = ALL_STATUSES.subList(0, startIndex); + List timedStatuses = ALL_STATUSES.subList(startIndex, stopIndex + 1); + + Context ctx = new Context(level, center, testRadius); + long[] timings = new long[timedStatuses.size()]; + + int testDiameter = 2 * testRadius + 1; + int numPositions = testDiameter * testDiameter; + ChunkPos[] testPositions = new ChunkPos[numPositions]; + CompoundTag[] snapshots = new CompoundTag[numPositions]; + ChunkAccess[][] neighborArrays = new ChunkAccess[numPositions][]; + + int idx = 0; + for (int tz = -testRadius; tz <= testRadius; tz++) { + for (int tx = -testRadius; tx <= testRadius; tx++) { + ChunkPos testPos = new ChunkPos(center.x + tx, center.z + tz); + testPositions[idx] = testPos; + neighborArrays[idx] = ctx.buildNeighborArray(testPos); + + ProtoChunk setupProto = ctx.newProtoChunk(testPos); + neighborArrays[idx][ctx.centerIndex] = setupProto; + for (ChunkStatus status : setupStatuses) { + status.generate(ctx.executor, level, ctx.generator, ctx.templates, + ctx.lightEngine, ctx.noopPromotion, Arrays.asList(neighborArrays[idx])).join(); + } + snapshots[idx] = ChunkSerializer.write(level, setupProto); + idx++; + ModernFix.LOGGER.info("worldgen benchmark setup progress: {}/{}", idx, numPositions); + } + } + + ModernFix.LOGGER.info("worldgen benchmark setup complete"); + + for (int iter = 0; iter < iterations; iter++) { + ModernFix.LOGGER.info("worldgen benchmark iteration: {}/{}", iter + 1, iterations); + for (int p = 0; p < numPositions; p++) { + ProtoChunk restored = ChunkSerializer.read( + level, ctx.poiManager, testPositions[p], snapshots[p]); + neighborArrays[p][ctx.centerIndex] = restored; + List neighborList = Arrays.asList(neighborArrays[p]); + + for (int s = 0; s < timedStatuses.size(); s++) { + long t0 = System.nanoTime(); + + timedStatuses.get(s).generate(ctx.executor, level, ctx.generator, + ctx.templates, ctx.lightEngine, ctx.noopPromotion, neighborList).join(); + + timings[s] += System.nanoTime() - t0; + } + } + } + + ModernFix.LOGGER.info("worldgen benchmark done"); + + ctx.cleanup(); + + return formatTimings(timedStatuses, timings, testRadius, iterations); + } + + private static String formatTimings(List statuses, long[] timings, int testRadius, int iterations) { + int totalChunks = (2 * testRadius + 1) * (2 * testRadius + 1) * iterations; + StringBuilder sb = new StringBuilder(); + long total = 0; + for (int i = 0; i < timings.length; i++) { + total += timings[i]; + String name = BuiltInRegistries.CHUNK_STATUS.getKey(statuses.get(i)).getPath(); + sb.append(String.format(" %-22s %8.1f ms (%6.2f ms/chunk)\n", + name, timings[i] / 1e6, timings[i] / 1e6 / totalChunks)); + } + sb.append(String.format(" %-22s %8.1f ms (%6.2f ms/chunk)\n", + "TOTAL", total / 1e6, total / 1e6 / totalChunks)); + return sb.toString(); + } + + private static class Context { + final ServerLevel level; + final ServerChunkCache chunkSource; + final ChunkPos center; + final int loadRadius; + final int loadDiameter; + final ChunkAccess[] realChunks; + final int neighborDiameter; + final int centerIndex; + final Executor executor; + final ChunkGenerator generator; + final ThreadedLevelLightEngine lightEngine; + final StructureTemplateManager templates; + final PoiManager poiManager; + final Function>> noopPromotion; + private final net.minecraft.core.Registry biomeRegistry; + + Context(ServerLevel level, ChunkPos center, int testRadius) { + this.level = level; + this.chunkSource = level.getChunkSource(); + this.center = center; + this.loadRadius = testRadius + REQUIRED_LOAD_RADIUS; + this.loadDiameter = 2 * loadRadius + 1; + this.neighborDiameter = 2 * REQUIRED_LOAD_RADIUS + 1; + this.centerIndex = neighborDiameter * neighborDiameter / 2; + this.executor = MoreExecutors.directExecutor(); + this.generator = chunkSource.getGenerator(); + this.lightEngine = chunkSource.getLightEngine(); + this.templates = level.getStructureManager(); + this.poiManager = chunkSource.getPoiManager(); + this.noopPromotion = chunk -> CompletableFuture.completedFuture(Either.left(chunk)); + this.biomeRegistry = level.registryAccess().registryOrThrow(Registries.BIOME); + + chunkSource.addRegionTicket(BENCHMARK_TICKET, center, loadRadius, Unit.INSTANCE); + + realChunks = new ChunkAccess[loadDiameter * loadDiameter]; + for (int dz = -loadRadius; dz <= loadRadius; dz++) { + for (int dx = -loadRadius; dx <= loadRadius; dx++) { + LevelChunk real = level.getChunk(center.x + dx, center.z + dz); + realChunks[(dz + loadRadius) * loadDiameter + (dx + loadRadius)] = + new ImposterProtoChunk(real, false); + } + } + } + + ProtoChunk newProtoChunk(ChunkPos pos) { + return new ProtoChunk(pos, UpgradeData.EMPTY, level, biomeRegistry, null); + } + + ChunkAccess[] buildNeighborArray(ChunkPos testPos) { + int count = neighborDiameter * neighborDiameter; + ChunkAccess[] array = new ChunkAccess[count]; + int baseX = (testPos.x - REQUIRED_LOAD_RADIUS) - (center.x - loadRadius); + int baseZ = (testPos.z - REQUIRED_LOAD_RADIUS) - (center.z - loadRadius); + for (int dz = 0; dz < neighborDiameter; dz++) { + System.arraycopy(realChunks, (baseZ + dz) * loadDiameter + baseX, + array, dz * neighborDiameter, neighborDiameter); + } + return array; + } + + void cleanup() { + chunkSource.removeRegionTicket(BENCHMARK_TICKET, center, loadRadius, Unit.INSTANCE); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java index 365c0757..5a4b37a7 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java +++ b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java @@ -5,6 +5,8 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import net.minecraft.commands.CommandSourceStack; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.Item; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkStatus; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.RegisterCommandsEvent; @@ -24,6 +26,7 @@ import net.minecraftforge.registries.RegisterEvent; import org.apache.commons.lang3.tuple.Pair; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.benchmark.WorldgenBenchmark; import org.embeddedt.modernfix.core.ModernFixMixinPlugin; import org.embeddedt.modernfix.forge.ModernFixConfig; import org.embeddedt.modernfix.forge.config.ConfigFixer; @@ -134,5 +137,12 @@ public class ModernFixForge { @SubscribeEvent(priority = EventPriority.LOWEST) public void onServerStarted(ServerStartedEvent event) { commonMod.onServerStarted(); + if (Boolean.getBoolean("modernfix.runWorldgenBenchmark")) { + int iterations = Integer.getInteger("modernfix.worldgenIterations", 100); + int testRadius = Integer.getInteger("modernfix.worldgenTestRadius", 10); + var level = event.getServer().overworld(); + ModernFix.LOGGER.info("Worldgen results: {}", WorldgenBenchmark.run(level, new ChunkPos(0, 0), testRadius, iterations, + ChunkStatus.SURFACE, ChunkStatus.SURFACE)); + } } } From 22915a91a11c06b21796eb3f14a3e53007b97c16 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:44:42 -0400 Subject: [PATCH 45/52] Implement a significantly more optimized biome lookup for surface rules --- .../BiomeManagerAccessor.java | 14 + .../SurfaceSystemMixin.java | 40 +++ .../modernfix/world/gen/ChunkBiomeLookup.java | 262 ++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/BiomeManagerAccessor.java create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/BiomeManagerAccessor.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/BiomeManagerAccessor.java new file mode 100644 index 00000000..7a6cf192 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/BiomeManagerAccessor.java @@ -0,0 +1,14 @@ +package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules; + +import net.minecraft.world.level.biome.BiomeManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(BiomeManager.class) +public interface BiomeManagerAccessor { + @Accessor("biomeZoomSeed") + long mfix$getZoomSeed(); + + @Accessor("noiseBiomeSource") + BiomeManager.NoiseBiomeSource mfix$getBiomeSource(); +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java new file mode 100644 index 00000000..6cbb2010 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java @@ -0,0 +1,40 @@ +package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules; + +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.NoiseChunk; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.SurfaceRules; +import net.minecraft.world.level.levelgen.SurfaceSystem; +import net.minecraft.world.level.levelgen.WorldGenerationContext; +import org.embeddedt.modernfix.world.gen.ChunkBiomeLookup; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.Function; + +@Mixin(SurfaceSystem.class) +public class SurfaceSystemMixin { + private static final ThreadLocal MFIX_LOOKUP_CACHE = ThreadLocal.withInitial(ChunkBiomeLookup::new); + + @ModifyArg(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V"), index = 4) + private Function> useFasterLookup(Function> biomeGetter, @Local(ordinal = 0, argsOnly = true) BiomeManager manager, @Local(ordinal = 0, argsOnly = true) ChunkAccess chunk) { + var lookup = MFIX_LOOKUP_CACHE.get(); + BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager; + lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager); + return lookup; + } + + @Inject(method = "buildSurface", at = @At("RETURN")) + private void disposeLookup(RandomState randomState, BiomeManager biomeManager, Registry biomes, boolean p_224652_, WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci) { + MFIX_LOOKUP_CACHE.get().dispose(); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java new file mode 100644 index 00000000..8c368c92 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java @@ -0,0 +1,262 @@ +package org.embeddedt.modernfix.world.gen; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.QuartPos; +import net.minecraft.util.LinearCongruentialGenerator; +import net.minecraft.util.Mth; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.chunk.ChunkAccess; + +import java.util.Arrays; +import java.util.function.Function; + +/** + * Drop-in replacement for {@code biomeManager::getBiome} in SurfaceSystem.buildSurface. + * + *

Pre-computes the Voronoi bias (fiddle) values and quart-resolution biome data for an + * entire chunk, then uses two optimizations: + *

    + *
  • Uniform check: If all 8 Voronoi candidate cells for a given quart position + * hold the same biome, the Voronoi computation is skipped entirely.
  • + *
  • Pre-computed bias: When the Voronoi is needed, the 48 LCG operations per block + * are replaced by array lookups of pre-computed fiddle values.
  • + *
+ */ +public class ChunkBiomeLookup implements Function> { + @SuppressWarnings("unchecked") + private Holder[] biomes = new Holder[0]; + private double[] biasX = new double[0], biasY = new double[0], biasZ = new double[0]; + private boolean[] uniform = new boolean[0]; + + private int qMinX, qMinY, qMinZ; + private int sizeX, sizeY, sizeZ; + + private BiomeManager fallbackManager; + + /** + * Pre-compute biome and bias data for the given chunk. Must be called before any + * {@link #getBiome} calls for positions within this chunk. + * + * @param source the underlying quart-resolution biome source (e.g. the chunk) + * @param biomeZoomSeed the obfuscated biome zoom seed from BiomeManager + */ + @SuppressWarnings("unchecked") + public void prepare(BiomeManager.NoiseBiomeSource source, long biomeZoomSeed, ChunkAccess chunk, BiomeManager fallback) { + int chunkMinX = chunk.getPos().getMinBlockX(); + int chunkMinZ = chunk.getPos().getMinBlockZ(); + int minBuildHeight = chunk.getMinBuildHeight(); + int maxBuildHeight = minBuildHeight + chunk.getHeight(); // exclusive + + // BiomeManager.getBiome subtracts a 2-block offset before converting to quart coords, + // then considers quart and quart+1 as the 8 Voronoi candidates. + int biomeOffset = 2; + int minBlockX = chunkMinX - biomeOffset; + int maxBlockX = chunkMinX + 15 - biomeOffset; + int minBlockZ = chunkMinZ - biomeOffset; + int maxBlockZ = chunkMinZ + 15 - biomeOffset; + int minBlockY = minBuildHeight - biomeOffset; + int maxBlockY = maxBuildHeight - 1 - biomeOffset; + + // Quart range: fromBlock(min) to fromBlock(max) + 1 (for the +1 Voronoi candidate) + this.qMinX = QuartPos.fromBlock(minBlockX); + int qMaxX = QuartPos.fromBlock(maxBlockX) + 1; + this.qMinZ = QuartPos.fromBlock(minBlockZ); + int qMaxZ = QuartPos.fromBlock(maxBlockZ) + 1; + this.qMinY = QuartPos.fromBlock(minBlockY); + int qMaxY = QuartPos.fromBlock(maxBlockY) + 1; + + this.sizeX = qMaxX - qMinX + 1; // always 6 for 16-wide chunks + this.sizeY = qMaxY - qMinY + 1; + this.sizeZ = qMaxZ - qMinZ + 1; + + int totalCells = sizeX * sizeY * sizeZ; + + // Reuse arrays across chunks if large enough + if (biomes.length < totalCells) { + biomes = new Holder[totalCells]; + biasX = new double[totalCells]; + biasY = new double[totalCells]; + biasZ = new double[totalCells]; + uniform = new boolean[totalCells]; + } + + // Fetch quart-resolution biomes + boolean isSingleBiome = !fetchBiomes(source); + + if (isSingleBiome) { + // All cells hold the same biome, so no need to do expensive computations. + Arrays.fill(uniform, 0, totalCells, true); + } else { + this.computeUniformity(); + this.computeBiases(biomeZoomSeed); + } + + this.fallbackManager = fallback; + } + + public void dispose() { + // Make sure we do not retain strong references to the biome holders + Arrays.fill(biomes, null); + } + + private boolean fetchBiomes(BiomeManager.NoiseBiomeSource source) { + var biomes = this.biomes; + Holder firstSeen = null; + boolean seenMultiple = false; + for (int rx = 0; rx < sizeX; rx++) { + int wx = qMinX + rx; + for (int rz = 0; rz < sizeZ; rz++) { + int wz = qMinZ + rz; + for (int ry = 0; ry < sizeY; ry++) { + int wy = qMinY + ry; + var biome = source.getNoiseBiome(wx, wy, wz); + biomes[index(rx, ry, rz)] = biome; + if (biome != firstSeen) { + if (firstSeen == null) { + firstSeen = biome; + } else { + seenMultiple = true; + } + } + } + } + } + return seenMultiple; + } + + private void computeUniformity() { + // For each quart position, check if all 8 Voronoi candidates hold the same biome. + // If so, the Voronoi result is guaranteed to be that biome regardless of fractional + // position, so we can skip the distance computation entirely. + var uniform = this.uniform; + int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ; + + for (int rx = 0; rx < sizeX - 1; rx++) { + for (int rz = 0; rz < sizeZ - 1; rz++) { + for (int ry = 0; ry < sizeY - 1; ry++) { + uniform[index(rx, ry, rz)] = isUniform(rx, ry, rz); + } + } + } + } + + private void computeBiases(long biomeZoomSeed) { + int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ; + int qMinX = this.qMinX, qMinY = this.qMinY, qMinZ = this.qMinZ; + + // Pre-compute bias (fiddle) values for the Voronoi distance computation. + for (int rx = 0; rx < sizeX; rx++) { + int wx = qMinX + rx; + for (int rz = 0; rz < sizeZ; rz++) { + int wz = qMinZ + rz; + for (int ry = 0; ry < sizeY; ry++) { + computeBias(index(rx, ry, rz), biomeZoomSeed, wx, qMinY + ry, wz); + } + } + } + } + + private void computeBias(int idx, long seed, int x, int y, int z) { + // Reproduces the LCG chain from BiomeManager.getFiddledDistance exactly + long s = LinearCongruentialGenerator.next(seed, x); + s = LinearCongruentialGenerator.next(s, y); + s = LinearCongruentialGenerator.next(s, z); + s = LinearCongruentialGenerator.next(s, x); + s = LinearCongruentialGenerator.next(s, y); + s = LinearCongruentialGenerator.next(s, z); + biasX[idx] = getFiddle(s); + s = LinearCongruentialGenerator.next(s, seed); + biasY[idx] = getFiddle(s); + s = LinearCongruentialGenerator.next(s, seed); + biasZ[idx] = getFiddle(s); + } + + private static double getFiddle(long seed) { + double d = (double) Math.floorMod(seed >> 24, 1024) / 1024.0D; + return (d - 0.5D) * 0.9D; + } + + private boolean isUniform(int rx, int ry, int rz) { + var biomes = this.biomes; + Holder ref = biomes[index(rx, ry, rz)]; + for (int dx = 0; dx <= 1; dx++) { + for (int dy = 0; dy <= 1; dy++) { + for (int dz = 0; dz <= 1; dz++) { + if (biomes[index(rx + dx, ry + dy, rz + dz)] != ref) { + return false; + } + } + } + } + return true; + } + + private int index(int rx, int ry, int rz) { + return (rx * sizeY + ry) * sizeZ + rz; + } + + @Override + public Holder apply(BlockPos pos) { + return getBiome(pos); + } + + public Holder getBiome(BlockPos pos) { + int i = pos.getX() - 2; + int j = pos.getY() - 2; + int k = pos.getZ() - 2; + + int rx = QuartPos.fromBlock(i) - qMinX; + int ry = QuartPos.fromBlock(j) - qMinY; + int rz = QuartPos.fromBlock(k) - qMinZ; + + if (rx < 0 || rx >= sizeX - 1 || ry < 0 || ry >= sizeY - 1 || rz < 0 || rz >= sizeZ - 1) { + return fallbackManager.getBiome(pos); + } + + int baseIdx = index(rx, ry, rz); + if (uniform[baseIdx]) { + return biomes[baseIdx]; + } + + return getBiomeWithVoronoi(i, j, k, rx, ry, rz); + } + + private Holder getBiomeWithVoronoi(int i, int j, int k, int rx, int ry, int rz) { + var biasX = this.biasX; + var biasY = this.biasY; + var biasZ = this.biasZ; + + double d0 = (double) QuartPos.quartLocal(i) / 4.0D; + double d1 = (double) QuartPos.quartLocal(j) / 4.0D; + double d2 = (double) QuartPos.quartLocal(k) / 4.0D; + + int closestIdx = 0; + double closestDist = Double.POSITIVE_INFINITY; + + for (int c = 0; c < 8; c++) { + boolean fx = (c & 4) == 0; + boolean fy = (c & 2) == 0; + boolean fz = (c & 1) == 0; + + int idx = index( + rx + (fx ? 0 : 1), + ry + (fy ? 0 : 1), + rz + (fz ? 0 : 1) + ); + + double dx = (fx ? d0 : d0 - 1.0D) + biasX[idx]; + double dy = (fy ? d1 : d1 - 1.0D) + biasY[idx]; + double dz = (fz ? d2 : d2 - 1.0D) + biasZ[idx]; + double dist = Mth.square(dx) + Mth.square(dy) + Mth.square(dz); + + if (dist < closestDist) { + closestDist = dist; + closestIdx = idx; + } + } + + return biomes[closestIdx]; + } +} From dbe9acb3d842f922e739e67a697d88c5ae5a4fc4 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 14 Mar 2026 21:40:16 -0400 Subject: [PATCH 46/52] Heavily optimize the BlockColumn impl used during surface rule evaluation --- .../SurfaceSystemMixin.java | 44 +++++- .../modernfix/forge/init/ModernFixForge.java | 2 +- .../world/gen/PrefetchingBlockColumn.java | 126 ++++++++++++++++++ 3 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/embeddedt/modernfix/world/gen/PrefetchingBlockColumn.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java index 6cbb2010..aa26d345 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java @@ -1,11 +1,14 @@ package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules; import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.Registry; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.chunk.BlockColumn; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.levelgen.NoiseChunk; import net.minecraft.world.level.levelgen.RandomState; @@ -13,10 +16,12 @@ import net.minecraft.world.level.levelgen.SurfaceRules; import net.minecraft.world.level.levelgen.SurfaceSystem; import net.minecraft.world.level.levelgen.WorldGenerationContext; import org.embeddedt.modernfix.world.gen.ChunkBiomeLookup; +import org.embeddedt.modernfix.world.gen.PrefetchingBlockColumn; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.function.Function; @@ -24,17 +29,52 @@ import java.util.function.Function; @Mixin(SurfaceSystem.class) public class SurfaceSystemMixin { private static final ThreadLocal MFIX_LOOKUP_CACHE = ThreadLocal.withInitial(ChunkBiomeLookup::new); + private static final ThreadLocal MFIX_BLOCK_COLUMN = new ThreadLocal<>(); @ModifyArg(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V"), index = 4) - private Function> useFasterLookup(Function> biomeGetter, @Local(ordinal = 0, argsOnly = true) BiomeManager manager, @Local(ordinal = 0, argsOnly = true) ChunkAccess chunk) { + private Function> useFasterLookup(Function> biomeGetter, + @Local(ordinal = 0, argsOnly = true) BiomeManager manager, + @Local(ordinal = 0, argsOnly = true) ChunkAccess chunk, + @Share("chunkBiomeLookup") LocalRef lookupRef) { var lookup = MFIX_LOOKUP_CACHE.get(); BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager; lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager); + lookupRef.set(lookup); return lookup; } @Inject(method = "buildSurface", at = @At("RETURN")) - private void disposeLookup(RandomState randomState, BiomeManager biomeManager, Registry biomes, boolean p_224652_, WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci) { + private void finishAndDisposeLookups(RandomState randomState, BiomeManager biomeManager, Registry biomes, boolean p_224652_, WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci) { MFIX_LOOKUP_CACHE.get().dispose(); + var column = MFIX_BLOCK_COLUMN.get(); + if (column != null) { + column.dispose(); + } + } + + @Redirect(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/biome/BiomeManager;getBiome(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/core/Holder;")) + private Holder useFasterLookup(BiomeManager instance, BlockPos pos, @Share("chunkBiomeLookup") LocalRef lookupRef) { + return lookupRef.get().apply(pos); + } + + @Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V")) + private void captureRealBlockColumn(CallbackInfo ci, @Local(ordinal = 0) LocalRef column, + @Local(ordinal = 0, argsOnly = true) ChunkAccess chunk, + @Share("prefetchColumn") LocalRef prefetchRef) { + var prefetchingBlockColumn = MFIX_BLOCK_COLUMN.get(); + if (prefetchingBlockColumn == null || prefetchingBlockColumn.getExpectedHeight() != chunk.getHeight()) { + prefetchingBlockColumn = new PrefetchingBlockColumn(chunk.getHeight()); + MFIX_BLOCK_COLUMN.set(prefetchingBlockColumn); + } + column.set(prefetchingBlockColumn); + prefetchRef.set(prefetchingBlockColumn); + } + + @Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos$MutableBlockPos;setZ(I)Lnet/minecraft/core/BlockPos$MutableBlockPos;", ordinal = 0, shift = At.Shift.AFTER)) + private void prefetchBlockArray(RandomState randomState, BiomeManager biomeManager, Registry biomes, boolean p_224652_, + WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci, + @Local(ordinal = 0) BlockColumn column, + @Local(ordinal = 0) BlockPos.MutableBlockPos cursor) { + ((PrefetchingBlockColumn)column).prefetch(chunk, cursor.getX() & 15, cursor.getZ() & 15); } } diff --git a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java index 5a4b37a7..4030eca6 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java +++ b/src/main/java/org/embeddedt/modernfix/forge/init/ModernFixForge.java @@ -138,7 +138,7 @@ public class ModernFixForge { public void onServerStarted(ServerStartedEvent event) { commonMod.onServerStarted(); if (Boolean.getBoolean("modernfix.runWorldgenBenchmark")) { - int iterations = Integer.getInteger("modernfix.worldgenIterations", 100); + int iterations = Integer.getInteger("modernfix.worldgenIterations", 15); int testRadius = Integer.getInteger("modernfix.worldgenTestRadius", 10); var level = event.getServer().overworld(); ModernFix.LOGGER.info("Worldgen results: {}", WorldgenBenchmark.run(level, new ChunkPos(0, 0), testRadius, iterations, diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/PrefetchingBlockColumn.java b/src/main/java/org/embeddedt/modernfix/world/gen/PrefetchingBlockColumn.java new file mode 100644 index 00000000..9a6c80f2 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/world/gen/PrefetchingBlockColumn.java @@ -0,0 +1,126 @@ +package org.embeddedt.modernfix.world.gen; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.BlockColumn; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.Heightmap; + +/** + * Wraps a BlockColumn and prefetches all block states for a column into an array. + * + *

Writes bypass {@link ChunkAccess#setBlockState} and go directly to the section, + * skipping heightmap and light updates that are unnecessary during the surface building + * stage (which runs before {@code INITIALIZE_LIGHT}). + */ +public class PrefetchingBlockColumn implements BlockColumn { + private static final BlockState AIR = Blocks.AIR.defaultBlockState(); + private static final BlockState VOID_AIR = Blocks.VOID_AIR.defaultBlockState(); + + private final BlockState[] states; + private final BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + + private ChunkAccess chunk; + private LevelChunkSection[] sections; + private int minBuildHeight; + private int localX, localZ; + + public PrefetchingBlockColumn(int height) { + this.states = new BlockState[height]; + } + + public int getExpectedHeight() { + return this.states.length; + } + + /** + * Prefetch all block states for the column at the given local XZ coordinates. + * Must be called before any getBlock/setBlock calls for this column. + */ + public void prefetch(ChunkAccess chunk, int localX, int localZ) { + if (chunk.getHeight() != this.states.length) { + throw new IllegalStateException(); + } + this.chunk = chunk; + this.sections = chunk.getSections(); + this.minBuildHeight = chunk.getMinBuildHeight(); + this.localX = localX; + this.localZ = localZ; + var sections = this.sections; + var states = this.states; + int offset = 0; + for (LevelChunkSection section : sections) { + if (section.hasOnlyAir()) { + for (int y = 0; y < 16; y++) { + states[offset + y] = AIR; + } + } else { + var container = section.getStates(); + for (int y = 0; y < 16; y++) { + states[offset + y] = container.get(localX, y, localZ); + } + } + offset += 16; + } + } + + /** + * Clear cached references to allow GC of the chunk between uses. + */ + public void dispose() { + this.chunk = null; + this.sections = null; + } + + @Override + public BlockState getBlock(int y) { + int idx = y - minBuildHeight; + if (idx >= 0 && idx < states.length) { + return states[idx]; + } + return VOID_AIR; + } + + private void markPostprocessing(int y) { + cursor.set( + SectionPos.sectionToBlockCoord(chunk.getPos().x, localX), + y, + SectionPos.sectionToBlockCoord(chunk.getPos().z, localZ) + ); + chunk.markPosForPostprocessing(cursor); + } + + private void updateHeightmap(Heightmap.Types type, int y, BlockState newState) { + chunk.getOrCreateHeightmapUnprimed(type).update(localX, y, localZ, newState); + } + + @Override + public void setBlock(int y, BlockState state) { + int idx = y - minBuildHeight; + var states = this.states; + if (idx >= 0 && idx < states.length) { + BlockState oldState = states[idx]; + if (oldState == state) { + return; + } + states[idx] = state; + // Write directly to the section, bypassing ProtoChunk.setBlockState which + // does expensive heightmap updates that are not needed during surface building. + int sectionIdx = idx >> 4; + sections[sectionIdx].setBlockState(localX, y & 15, localZ, state, false); + if (!state.getFluidState().isEmpty()) { + markPostprocessing(y); + } + // Update heightmaps if the air/motion-blocking properties changed. + if (oldState.isAir() != state.isAir()) { + updateHeightmap(Heightmap.Types.WORLD_SURFACE_WG, y, state); + } + if (oldState.blocksMotion() != state.blocksMotion()) { + updateHeightmap(Heightmap.Types.OCEAN_FLOOR_WG, y, state); + } + } + } +} From 1794c81b6193b591465bff7c8db4acbf3c9e168a Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:12:06 -0400 Subject: [PATCH 47/52] Optimize sequence rules that check many biome conditions in a row --- .../SequenceRuleSourceMixin.java | 19 ++++ .../world/gen/SurfaceRuleOptimizer.java | 100 ++++++++++++++++++ .../resources/META-INF/accesstransformer.cfg | 8 ++ 3 files changed, 127 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java new file mode 100644 index 00000000..a599aa7b --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java @@ -0,0 +1,19 @@ +package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules; + +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.embeddedt.modernfix.world.gen.SurfaceRuleOptimizer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(targets = {"net/minecraft/world/level/levelgen/SurfaceRules$SequenceRuleSource"}) +public class SequenceRuleSourceMixin { + @Inject(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At("HEAD"), cancellable = true) + private void optimizeApply(SurfaceRules.Context context, CallbackInfoReturnable cir) { + var optimized = SurfaceRuleOptimizer.optimizeSequenceRule((SurfaceRules.SequenceRuleSource)(Object) this, context); + if (optimized != null) { + cir.setReturnValue(optimized); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java b/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java new file mode 100644 index 00000000..42492fa1 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java @@ -0,0 +1,100 @@ +package org.embeddedt.modernfix.world.gen; + +import it.unimi.dsi.fastutil.objects.Reference2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.core.Holder; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class SurfaceRuleOptimizer { + public static @Nullable SurfaceRules.SurfaceRule optimizeSequenceRule(SurfaceRules.SequenceRuleSource source, SurfaceRules.Context context) { + // First pass: collect which biomes appear and count biome-gated branches + Reference2ObjectOpenHashMap, List> perBiomeSources = new Reference2ObjectOpenHashMap<>(); + int biomeGatedBranches = 0; + for (var innerSource : source.sequence()) { + if (innerSource instanceof SurfaceRules.TestRuleSource testRuleSource + && testRuleSource.ifTrue() instanceof SurfaceRules.BiomeConditionSource biomeConditionSource) { + biomeGatedBranches++; + for (var biome : biomeConditionSource.biomes) { + perBiomeSources.putIfAbsent(biome, new ArrayList<>()); + } + } + } + if (biomeGatedBranches < 3) { + return null; + } + // Second pass: build per-biome source lists preserving original interleaving order + List noMatchSources = new ArrayList<>(); + for (var innerSource : source.sequence()) { + if (innerSource instanceof SurfaceRules.TestRuleSource testRuleSource + && testRuleSource.ifTrue() instanceof SurfaceRules.BiomeConditionSource biomeConditionSource) { + // Add the inner rule (condition stripped) only to the matching biomes' lists + for (var biome : biomeConditionSource.biomes) { + perBiomeSources.get(biome).add(testRuleSource.thenRun()); + } + } else { + // Non-biome-gated rule: add to every biome list and the no-match list + for (var list : perBiomeSources.values()) { + list.add(innerSource); + } + noMatchSources.add(innerSource); + } + } + // Compile all source lists into rule lists + Reference2ObjectOpenHashMap, List> compiledBiomeMatch = new Reference2ObjectOpenHashMap<>(perBiomeSources.size()); + Reference2ObjectMaps.fastForEach(perBiomeSources, entry -> { + List compiled = new ArrayList<>(entry.getValue().size()); + for (var src : entry.getValue()) { + compiled.add(src.apply(context)); + } + compiledBiomeMatch.put(entry.getKey(), List.copyOf(compiled)); + }); + List compiledNoMatch = new ArrayList<>(noMatchSources.size()); + for (var src : noMatchSources) { + compiledNoMatch.add(src.apply(context)); + } + return new OptimizedBiomeLookupSequenceRule(compiledBiomeMatch, List.copyOf(compiledNoMatch), context); + } + + public record OptimizedBiomeLookupSequenceRule( + Map, List> rulesForBiomeMatch, + List rulesForNoBiomeMatch, + SurfaceRules.Context context + ) implements SurfaceRules.SurfaceRule { + @Override + public @Nullable BlockState tryApply(int x, int y, int z) { + var biome = context.biome.get(); + var key = (biome instanceof Holder.Reference ref) ? ref.key() : biome.unwrapKey().orElseThrow(); + var ruleList = rulesForBiomeMatch.getOrDefault(key, rulesForNoBiomeMatch); + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < ruleList.size(); i++) { + var rule = ruleList.get(i); + var state = rule.tryApply(x, y, z); + if (state != null) { + return state; + } + } + return null; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + OptimizedBiomeLookupSequenceRule that = (OptimizedBiomeLookupSequenceRule) o; + return rulesForBiomeMatch.equals(that.rulesForBiomeMatch) && rulesForNoBiomeMatch.equals(that.rulesForNoBiomeMatch); + } + + @Override + public int hashCode() { + return Objects.hash(rulesForBiomeMatch, rulesForNoBiomeMatch); + } + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 13834361..c7980896 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -5,6 +5,14 @@ public net.minecraft.client.renderer.block.model.multipart.MultiPart f_111962_ public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl (Lnet/minecraft/client/resources/model/ModelBakery;Ljava/util/function/BiFunction;Lnet/minecraft/resources/ResourceLocation;)V public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRule +public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource +public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource (Ljava/util/List;)V +public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource +public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource (Lnet/minecraft/world/level/levelgen/SurfaceRules$ConditionSource;Lnet/minecraft/world/level/levelgen/SurfaceRules$RuleSource;)V +public net.minecraft.world.level.levelgen.SurfaceRules$BiomeConditionSource +public net.minecraft.world.level.levelgen.SurfaceRules$BiomeConditionSource (Ljava/util/List;)V +public net.minecraft.world.level.levelgen.SurfaceRules$BiomeConditionSource f_189489_ +public net.minecraft.world.level.levelgen.SurfaceRules$Context f_189555_ public net.minecraft.client.renderer.block.model.BlockModel f_111415_ public net.minecraft.server.packs.resources.ProfiledReloadInstance$State f_10689_ public net.minecraft.server.packs.resources.ProfiledReloadInstance$State f_10690_ From 53349cbd1a08076c56a1c35a39cbd77964184233 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:14:35 -0400 Subject: [PATCH 48/52] Remove skip_redundant_saves --- .../ChunkHolderMixin.java | 27 -------------- .../ChunkMapAccessor.java | 12 ------- .../ChunkSerializerMixin.java | 26 -------------- .../ThreadedLevelLightEngineMixin.java | 36 ------------------- 4 files changed, 101 deletions(-) delete mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkHolderMixin.java delete mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkMapAccessor.java delete mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkSerializerMixin.java delete mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ThreadedLevelLightEngineMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkHolderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkHolderMixin.java deleted file mode 100644 index 4baa7b3d..00000000 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkHolderMixin.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.bugfix.skip_redundant_saves; - -import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; -import net.minecraft.server.level.ChunkHolder; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.LevelChunk; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; - -import javax.annotation.Nullable; - -@Mixin(ChunkHolder.class) -public abstract class ChunkHolderMixin { - @Shadow - @Nullable - public abstract LevelChunk getTickingChunk(); - - /** - * @author embeddedt - * @reason prevent chunks from being flagged for saving when light engine is loading data from disk - */ - @WrapWithCondition(method = "sectionLightChanged", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/ChunkAccess;setUnsaved(Z)V")) - private boolean onlyMarkUnsavedIfAlreadyTicking(ChunkAccess instance, boolean unsaved) { - return this.getTickingChunk() != null; - } -} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkMapAccessor.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkMapAccessor.java deleted file mode 100644 index 649d1c28..00000000 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkMapAccessor.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.bugfix.skip_redundant_saves; - -import net.minecraft.server.level.ChunkMap; -import net.minecraft.world.level.ChunkPos; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Invoker; - -@Mixin(ChunkMap.class) -public interface ChunkMapAccessor { - @Invoker("releaseLightTicket") - void mfix$invokeReleaseLightTicket(ChunkPos pos); -} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkSerializerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkSerializerMixin.java deleted file mode 100644 index a1574349..00000000 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ChunkSerializerMixin.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.bugfix.skip_redundant_saves; - -import com.llamalad7.mixinextras.sugar.Local; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.entity.ai.village.poi.PoiManager; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.ProtoChunk; -import net.minecraft.world.level.chunk.storage.ChunkSerializer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(ChunkSerializer.class) -public class ChunkSerializerMixin { - /** - * @author embeddedt - * @reason When reloading chunks from disk, they by definition normally don't need saving unless they've changed. - */ - @Inject(method = "read", at = @At(value = "CONSTANT", args = "stringValue=PostProcessing", ordinal = 0)) - private static void updateUnsavedFlag(ServerLevel level, PoiManager poiManager, ChunkPos pos, CompoundTag tag, CallbackInfoReturnable cir, @Local(ordinal = 0) ChunkAccess chunkaccess) { - chunkaccess.setUnsaved(tag.getBoolean("shouldSave")); - } -} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ThreadedLevelLightEngineMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ThreadedLevelLightEngineMixin.java deleted file mode 100644 index 4ffa7362..00000000 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/skip_redundant_saves/ThreadedLevelLightEngineMixin.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.bugfix.skip_redundant_saves; - -import net.minecraft.server.level.ChunkMap; -import net.minecraft.server.level.ThreadedLevelLightEngine; -import net.minecraft.world.level.chunk.ChunkAccess; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.util.concurrent.CompletableFuture; - -@Mixin(ThreadedLevelLightEngine.class) -public class ThreadedLevelLightEngineMixin { - @Shadow - @Final - private ChunkMap chunkMap; - - /** - * @author embeddedt - * @reason avoid toggling the lightCorrect flag when chunk is already lit, because it triggers saving - */ - @Inject(method = "lightChunk", at = @At("HEAD"), cancellable = true) - private void skipLightCorrectFlagChange(ChunkAccess chunk, boolean isAlreadyLit, CallbackInfoReturnable> cir) { - if (isAlreadyLit) { - ((ChunkMapAccessor)this.chunkMap).mfix$invokeReleaseLightTicket(chunk.getPos()); - // Defensively ensure the lightCorrect flag is set properly on exit from this method - if (!chunk.isLightCorrect()) { - chunk.setLightCorrect(true); - } - cir.setReturnValue(CompletableFuture.completedFuture(chunk)); - } - } -} From 670e06816bd868bd4a181b845b484099e853bcd9 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:15:44 -0400 Subject: [PATCH 49/52] Reduce work done while waiting for singleplayer client to initiate connection --- .../IntegratedServerMixin.java | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java index 00791452..57cd9f8b 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java @@ -1,21 +1,39 @@ package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.datafixers.DataFixer; import net.minecraft.client.Minecraft; import net.minecraft.client.server.IntegratedServer; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.Services; +import net.minecraft.server.WorldStem; +import net.minecraft.server.level.progress.ChunkProgressListenerFactory; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.world.level.storage.LevelStorageSource; import org.embeddedt.modernfix.annotation.ClientOnlyMixin; import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; +import java.net.Proxy; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; @Mixin(IntegratedServer.class) @ClientOnlyMixin -public abstract class IntegratedServerMixin implements IDeferrableIntegratedServer { +public abstract class IntegratedServerMixin extends MinecraftServer implements IDeferrableIntegratedServer { + @Shadow + private boolean paused; + private final AtomicBoolean mfix$hasPrimaryClientJoined = new AtomicBoolean(false); + public IntegratedServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory) { + super(serverThread, storageSource, packRepository, worldStem, proxy, fixerUpper, services, progressListenerFactory); + } + /** * @author embeddedt * @reason Wait to be finished processing all expensive packets (recipes, tags, etc.) @@ -26,6 +44,23 @@ public abstract class IntegratedServerMixin implements IDeferrableIntegratedServ return !mfix$hasPrimaryClientJoined.get() || original.call(instance); } + /** + * @author embeddedt + * @reason If waiting for a client connection to exist, we only need to tick the server connection, + * not the whole server as vanilla does. + */ + @WrapWithCondition(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickServer(Ljava/util/function/BooleanSupplier;)V", ordinal = 0)) + private boolean preventRunningFullServerTick(MinecraftServer server, BooleanSupplier hasTimeLeft) { + if (this.paused && !mfix$hasPrimaryClientJoined.get()) { + var conn = this.getConnection(); + if (conn != null) { + conn.tick(); + } + return false; + } + return true; + } + @Override public void mfix$markClientLoadFinished() { mfix$hasPrimaryClientJoined.set(true); From a9340b2642ccd718ec8c1851ffd8a12cf449b2d5 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:11:11 -0400 Subject: [PATCH 50/52] Rewrite and improve mixin.perf.cache_strongholds --- .../ChunkGeneratorMixin.java | 179 ++++++++++++++---- .../cache_strongholds/ServerLevelMixin.java | 61 ++---- .../modernfix/duck/IChunkGenerator.java | 6 +- .../modernfix/duck/IServerLevel.java | 7 - .../world/StrongholdLocationCache.java | 54 ------ 5 files changed, 159 insertions(+), 148 deletions(-) delete mode 100644 src/main/java/org/embeddedt/modernfix/duck/IServerLevel.java delete mode 100644 src/main/java/org/embeddedt/modernfix/world/StrongholdLocationCache.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java index f9ca5195..65d5b383 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java @@ -1,68 +1,169 @@ package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import net.minecraft.Util; import net.minecraft.core.Holder; -import net.minecraft.server.level.ServerLevel; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.*; +import net.minecraft.resources.RegistryOps; import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; import net.minecraft.world.level.levelgen.structure.StructureSet; import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.duck.IChunkGenerator; -import org.embeddedt.modernfix.duck.IServerLevel; -import org.embeddedt.modernfix.world.StrongholdLocationCache; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.Shadow; -import java.lang.ref.WeakReference; +import java.lang.ref.SoftReference; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; @Mixin(ChunkGeneratorStructureState.class) public class ChunkGeneratorMixin implements IChunkGenerator { - private WeakReference mfix$serverLevel; + @Shadow + @Final + private long concentricRingsSeed; + + @Shadow + @Final + private BiomeSource biomeSource; + + private Path mfix$dimensionPath; + private RegistryAccess.Frozen mfix$registryAccess; + private SoftReference>> mfix$cachedPositions = new SoftReference<>(null); + + private static final String CACHE_FILENAME = "mfix_stronghold_cache_v2.nbt"; @Override - public void mfix$setAssociatedServerLevel(ServerLevel level) { - mfix$serverLevel = new WeakReference<>(level); + public void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess) { + this.mfix$dimensionPath = cachePath; + this.mfix$registryAccess = registryAccess; } - @Inject(method = "generateRingPositions", at = @At("HEAD"), cancellable = true) - private void useCachedDataIfAvailable(Holder structureSet, ConcentricRingsStructurePlacement placement, CallbackInfoReturnable>> cir) { - if(placement.count() == 0) - return; - ServerLevel level = searchLevel(); - if(level == null) - return; - StrongholdLocationCache cache = ((IServerLevel)level).mfix$getStrongholdCache(); - List positions = cache.getChunkPosList(); - if(positions.isEmpty()) - return; - ModernFix.LOGGER.debug("Loaded stronghold cache for dimension {} with {} positions", level.dimension().location(), positions.size()); - cir.setReturnValue(CompletableFuture.completedFuture(positions)); + @WrapMethod(method = "generateRingPositions") + private CompletableFuture> modernfix$cacheRingPositions(Holder structureSet, + ConcentricRingsStructurePlacement placement, + Operation>> original) { + if (this.mfix$registryAccess == null || this.mfix$dimensionPath == null) { + return original.call(structureSet, placement); + } + + String cacheKey = mfix$makeCacheKey(placement); + + // Try reading from cache + List cached = mfix$readFromCache(cacheKey); + if (cached != null) { + ModernFix.LOGGER.debug("Using cached stronghold positions for {}", cacheKey); + return CompletableFuture.completedFuture(List.copyOf(cached)); + } + + return original.call(structureSet, placement).thenApplyAsync(positions -> { + mfix$writeToCache(cacheKey, positions); + return positions; + }, Util.ioPool()); } - private ServerLevel searchLevel() { - if(mfix$serverLevel != null) - return mfix$serverLevel.get(); - else + private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) { + RegistryOps ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$registryAccess); + String placementKey = ConcentricRingsStructurePlacement.CODEC.encodeStart(ops, placement) + .result().map(Tag::toString).orElse(null); + String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource) + .result().map(Tag::toString).orElse(null); + if (placementKey == null || biomeSourceKey == null) { + ModernFix.LOGGER.warn("Failed to create cache key for concentric structure placement"); return null; + } + String data = placementKey + ";biomes=" + biomeSourceKey + ";seed=" + this.concentricRingsSeed; + try { + byte[] hash = MessageDigest.getInstance("SHA-256").digest(data.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(64); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + return null; + } } - @Inject(method = "generateRingPositions", at = @At("RETURN"), cancellable = true) - private void saveCachedData(Holder structureSet, ConcentricRingsStructurePlacement placement, CallbackInfoReturnable>> cir) { - cir.setReturnValue(cir.getReturnValue().thenApplyAsync(list -> { - if(list.size() == 0) - return list; - ServerLevel level = searchLevel(); - if(level != null) { - StrongholdLocationCache cache = ((IServerLevel)level).mfix$getStrongholdCache(); - cache.setChunkPosList(list); - ModernFix.LOGGER.debug("Saved stronghold cache for dimension {}", level.dimension().location()); + private synchronized List mfix$readFromCache(String cacheKey) { + Map> cache = mfix$getOrLoadCache(); + return cache.get(cacheKey); + } + + private synchronized void mfix$writeToCache(String cacheKey, List positions) { + Map> cache = mfix$getOrLoadCache(); + cache.put(cacheKey, List.copyOf(positions)); + mfix$cachedPositions = new SoftReference<>(cache); + mfix$saveCacheFile(cache); + } + + private Map> mfix$getOrLoadCache() { + Map> cache = mfix$cachedPositions.get(); + if (cache != null) { + return cache; + } + cache = mfix$loadCacheFile(); + mfix$cachedPositions = new SoftReference<>(cache); + return cache; + } + + private Map> mfix$loadCacheFile() { + Path file = mfix$dimensionPath.resolve(CACHE_FILENAME); + if (!Files.exists(file)) { + return new HashMap<>(); + } + try { + CompoundTag root = NbtIo.readCompressed(file.toFile()); + Map> result = new HashMap<>(); + for (String key : root.getAllKeys()) { + if (root.contains(key, Tag.TAG_INT_ARRAY)) { + int[] data = root.getIntArray(key); + if (data.length >= 2 && data.length % 2 == 0) { + List positions = new ArrayList<>(data.length / 2); + for (int i = 0; i < data.length; i += 2) { + positions.add(new ChunkPos(data[i], data[i + 1])); + } + result.put(key, positions); + } + } } - return list; - }, Util.backgroundExecutor())); + return result; + } catch (Exception e) { + ModernFix.LOGGER.warn("Failed to read stronghold cache, will recompute", e); + return new HashMap<>(); + } + } + + private void mfix$saveCacheFile(Map> cache) { + CompoundTag root = new CompoundTag(); + for (var entry : cache.entrySet()) { + List positions = entry.getValue(); + int[] data = new int[positions.size() * 2]; + for (int i = 0; i < positions.size(); i++) { + ChunkPos pos = positions.get(i); + data[i * 2] = pos.x; + data[i * 2 + 1] = pos.z; + } + root.putIntArray(entry.getKey(), data); + } + Path file = mfix$dimensionPath.resolve(CACHE_FILENAME); + try { + NbtIo.writeCompressed(root, file.toFile()); + } catch (Exception e) { + ModernFix.LOGGER.warn("Failed to write stronghold cache", e); + } } } diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java index eba1767b..8505303e 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java @@ -1,61 +1,30 @@ package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds; -import net.minecraft.core.Holder; -import net.minecraft.core.RegistryAccess; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; import net.minecraft.resources.ResourceKey; -import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; -import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.level.Level; import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; -import net.minecraft.world.level.dimension.DimensionType; -import net.minecraft.world.level.storage.DimensionDataStorage; -import net.minecraft.world.level.storage.WritableLevelData; +import net.minecraft.world.level.storage.LevelStorageSource; import org.embeddedt.modernfix.duck.IChunkGenerator; -import org.embeddedt.modernfix.duck.IServerLevel; -import org.embeddedt.modernfix.world.StrongholdLocationCache; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.function.Supplier; @Mixin(ServerLevel.class) -public abstract class ServerLevelMixin extends Level implements IServerLevel { - protected ServerLevelMixin(WritableLevelData arg, ResourceKey arg2, RegistryAccess arg3, Holder arg4, Supplier supplier, boolean bl, boolean bl2, long l, int i) { - super(arg, arg2, arg3, arg4, supplier, bl, bl2, l, i); - } - - @Shadow public abstract DimensionDataStorage getDataStorage(); - - @Shadow @Final private ServerChunkCache chunkSource; - private StrongholdLocationCache mfix$strongholdCache; - +public class ServerLevelMixin { /** - * Initialize the stronghold cache but don't force any structure generation yet. + * @author embeddedt + * @reason Make the dimension path accessible to ChunkGeneratorStructureState. */ - @Redirect(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/ChunkGeneratorStructureState;ensureStructuresGenerated()V")) - private void hookStrongholdCache(ChunkGeneratorStructureState generator) { - ((IChunkGenerator)generator).mfix$setAssociatedServerLevel((ServerLevel)(Object)this); - } - - /** - * Now start the stronghold generation process. - */ - @Inject(method = "", at = @At("TAIL")) - private void ensureGeneration(CallbackInfo ci) { - mfix$strongholdCache = this.getDataStorage().computeIfAbsent(StrongholdLocationCache::load, - StrongholdLocationCache::new, - StrongholdLocationCache.getFileId(this.dimensionTypeRegistration())); - this.chunkSource.getGeneratorState().ensureStructuresGenerated(); - } - - @Override - public StrongholdLocationCache mfix$getStrongholdCache() { - return mfix$strongholdCache; + @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/ChunkGeneratorStructureState;ensureStructuresGenerated()V")) + private void setCachePath(ChunkGeneratorStructureState instance, Operation original, + @Local(ordinal = 0, argsOnly = true) LevelStorageSource.LevelStorageAccess levelStorageAccess, + @Local(ordinal = 0, argsOnly = true) ResourceKey dimension, + @Local(ordinal = 0, argsOnly = true) MinecraftServer server) { + ((IChunkGenerator)instance).mfix$setStrongholdCachePath(levelStorageAccess.getDimensionPath(dimension), server.registryAccess()); + original.call(instance); } } diff --git a/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java b/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java index 5f48ba9e..3cf83acc 100644 --- a/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java @@ -1,7 +1,9 @@ package org.embeddedt.modernfix.duck; -import net.minecraft.server.level.ServerLevel; +import net.minecraft.core.RegistryAccess; + +import java.nio.file.Path; public interface IChunkGenerator { - void mfix$setAssociatedServerLevel(ServerLevel level); + void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess); } diff --git a/src/main/java/org/embeddedt/modernfix/duck/IServerLevel.java b/src/main/java/org/embeddedt/modernfix/duck/IServerLevel.java deleted file mode 100644 index 34c6b0c8..00000000 --- a/src/main/java/org/embeddedt/modernfix/duck/IServerLevel.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.embeddedt.modernfix.duck; - -import org.embeddedt.modernfix.world.StrongholdLocationCache; - -public interface IServerLevel { - StrongholdLocationCache mfix$getStrongholdCache(); -} diff --git a/src/main/java/org/embeddedt/modernfix/world/StrongholdLocationCache.java b/src/main/java/org/embeddedt/modernfix/world/StrongholdLocationCache.java deleted file mode 100644 index 4ffafada..00000000 --- a/src/main/java/org/embeddedt/modernfix/world/StrongholdLocationCache.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.embeddedt.modernfix.world; - -import net.minecraft.core.Holder; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.Tag; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.dimension.DimensionType; -import net.minecraft.world.level.saveddata.SavedData; - -import java.util.ArrayList; -import java.util.List; - -public class StrongholdLocationCache extends SavedData { - private List chunkPosList; - public StrongholdLocationCache() { - super(); - chunkPosList = new ArrayList<>(); - } - - public List getChunkPosList() { - return new ArrayList<>(chunkPosList); - } - - public void setChunkPosList(List positions) { - this.chunkPosList = new ArrayList<>(positions); - this.setDirty(); - } - - public static StrongholdLocationCache load(CompoundTag arg) { - StrongholdLocationCache cache = new StrongholdLocationCache(); - if(arg.contains("Positions", Tag.TAG_LONG_ARRAY)) { - long[] positions = arg.getLongArray("Positions"); - for(long position : positions) { - cache.chunkPosList.add(new ChunkPos(position)); - } - } - return cache; - } - - @Override - public CompoundTag save(CompoundTag compoundTag) { - long[] serialized = new long[chunkPosList.size()]; - for(int i = 0; i < chunkPosList.size(); i++) { - ChunkPos thePos = chunkPosList.get(i); - serialized[i] = thePos.toLong(); - } - compoundTag.putLongArray("Positions", serialized); - return compoundTag; - } - - public static String getFileId(Holder dimensionType) { - return "mfix_strongholds"; - } -} From 18dc488ab9220fd8fef88026966e8a38d27e0f12 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:36:07 -0400 Subject: [PATCH 51/52] Avoid spinning in Minecraft.doWorldLoad --- .../MinecraftMixin.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/MinecraftMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/MinecraftMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/MinecraftMixin.java new file mode 100644 index 00000000..3d044b92 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/MinecraftMixin.java @@ -0,0 +1,24 @@ +package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load; + +import net.minecraft.client.Minecraft; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Minecraft.class) +@ClientOnlyMixin +public class MinecraftMixin { + /** + * @author embeddedt + * @reason spin-waiting burns CPU time on the main thread, when the server thread is likely to take some time + * to be ready. + */ + @Redirect(method = "doWorldLoad", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;yield()V")) + private void sleepInsteadOfYield() { + try { + Thread.sleep(16L); + } catch (InterruptedException ignored) { + } + } +} From 79d2b28d5b8098779874b01e4d46a9567ae788f0 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:25:37 -0400 Subject: [PATCH 52/52] Fix Forge handshake taking extremely long time with many payloads --- .../HandshakeHandlerMixin.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_handshake_stall/HandshakeHandlerMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_handshake_stall/HandshakeHandlerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_handshake_stall/HandshakeHandlerMixin.java new file mode 100644 index 00000000..a3c8f503 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/fix_handshake_stall/HandshakeHandlerMixin.java @@ -0,0 +1,56 @@ +package org.embeddedt.modernfix.common.mixin.perf.fix_handshake_stall; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraftforge.network.HandshakeHandler; +import net.minecraftforge.network.NetworkRegistry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Slice; + +import java.util.List; + +@Mixin(value = HandshakeHandler.class, remap = false) +public class HandshakeHandlerMixin { + @Shadow + private int packetPosition; + + @Shadow + private List messageList; + + @Shadow + private List sentMessages; + + /** + * @author embeddedt + * @reason Forge only sends one login payload per tick. It takes many seconds to send all the payloads at this rate. + * During this time, the game remains frozen on the chunk loading screen with almost zero CPU usage. + * To fix this, we re-tick the handshake handler until the packetPosition stops advancing or the handler indicates + * it no longer needs ticking. + */ + @WrapMethod(method = "tickServer") + private boolean modernfix$retick(Operation original) { + boolean isDoneTicking; + int prevPacketPosition; + do { + prevPacketPosition = this.packetPosition; + isDoneTicking = original.call(); + } while(!isDoneTicking && this.packetPosition > prevPacketPosition); + return isDoneTicking; + } + + /** + * @author embeddedt + * @reason The original HandshakeHandler has an off-by-one error in its completion check. We patch this to prevent + * our optimization from potentially triggering it more often due to the timing change. + */ + @WrapOperation(method = "tickServer", at = @At(value = "INVOKE", target = "Ljava/util/List;isEmpty()Z", ordinal = 0), slice = @Slice(from = @At(value = "INVOKE", target = "Ljava/util/List;removeIf(Ljava/util/function/Predicate;)Z", ordinal = 0))) + private boolean preventEarlyExit(List instance, Operation original) { + if (instance != this.sentMessages) { + throw new AssertionError("Injector is misplaced"); + } + return original.call(instance) && this.packetPosition >= this.messageList.size(); + } +}