From ab8a8068e057492c7f65f81599d8a48dd16ba0a8 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:58:18 -0400 Subject: [PATCH 01/10] Avoid synchronizing layer list in LivingEntityRenderer --- .../safety/LivingEntityRendererMixin.java | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/safety/LivingEntityRendererMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/safety/LivingEntityRendererMixin.java index 8e165b7b..c6dac5de 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/safety/LivingEntityRendererMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/safety/LivingEntityRendererMixin.java @@ -1,28 +1,34 @@ package org.embeddedt.modernfix.common.mixin.safety; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.client.Minecraft; +import net.minecraft.client.model.EntityModel; import net.minecraft.client.renderer.entity.LivingEntityRenderer; import net.minecraft.client.renderer.entity.layers.RenderLayer; +import net.minecraft.world.entity.Entity; +import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.annotation.ClientOnlyMixin; -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.Collections; -import java.util.List; @Mixin(LivingEntityRenderer.class) @ClientOnlyMixin -public class LivingEntityRendererMixin { - @Shadow @Final @Mutable - protected List> layers; +public abstract class LivingEntityRendererMixin> { + @Shadow + public abstract boolean addLayer(RenderLayer layer); - @Inject(method = "", at = @At("RETURN")) - private void synchronizeLayerList(CallbackInfo ci) { - /* allows buggy mods to call addLayer concurrently, order is not deterministic but can't fix that */ - this.layers = Collections.synchronizedList(layers); + /** + * @author embeddedt + * @reason avoid CMEs from buggy mods calling addLayer on wrong thread + */ + @WrapMethod(method = "addLayer") + private boolean handleOffThreadLayerAdd(RenderLayer layer, Operation original) { + if (!Minecraft.getInstance().isSameThread()) { + ModernFix.LOGGER.error("LivingEntityRenderer.addLayer called on wrong thread", new Exception()); + Minecraft.getInstance().tell(() -> this.addLayer(layer)); + return true; + } + return original.call(layer); } } From 94f1fbf4db048b5762d34137de1ee12c988b2274 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:05:05 -0400 Subject: [PATCH 02/10] Rewrite AttachCapabilitiesEvent hoisting to not rely on phases --- .../AttachCapabilitiesEventMixin.java | 65 ++++++++----------- .../ForgeEventFactoryMixin.java | 20 ++++++ .../modernfix/duck/IBatchingCapEvent.java | 5 ++ 3 files changed, 52 insertions(+), 38 deletions(-) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/bytecode_analysis/ForgeEventFactoryMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/duck/IBatchingCapEvent.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 index a5c168c4..139eca6c 100644 --- 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 @@ -7,64 +7,53 @@ 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.duck.IBatchingCapEvent; 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.Final; 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; +import java.util.HashMap; +import java.util.Map; @Mixin(AttachCapabilitiesEvent.class) -public abstract class AttachCapabilitiesEventMixin extends Event { - @Shadow - public abstract void addCapability(ResourceLocation key, ICapabilityProvider cap); +public abstract class AttachCapabilitiesEventMixin extends Event implements IBatchingCapEvent { + @Shadow @Final + private Map caps; @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; + private final Map mfix$phaseMap = new HashMap<>(); /** * @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 + * @reason record the current dispatch phase so we can sort within phase groups later */ @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)); - } + private void mfix$trackPhase(ResourceLocation key, ICapabilityProvider cap, Operation original) { + original.call(key, cap); + mfix$phaseMap.put(key, this.getPhase()); } @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(); - } + public void mfix$sortCaps() { + if (caps.size() < 2) { + return; } - super.setPhase(value); + var entries = new ArrayList<>(caps.entrySet()); + entries.sort(Comparator.comparingInt(e -> { + EventPriority phase = mfix$phaseMap.getOrDefault(e.getKey(), EventPriority.NORMAL); + var result = CapabilityAnalyzer.analyze(e.getValue().getClass()); + // Primary: preserve phase ordering (HIGHEST=0 .. LOWEST=4) + // Secondary: Known/AlwaysEmpty before Indeterminate within each phase + int capKey = result instanceof CapabilityAnalysisResult.Indeterminate ? 1 : 0; + return phase.ordinal() * 2 + capKey; + })); + caps.clear(); + entries.forEach(e -> caps.put(e.getKey(), e.getValue())); + mfix$phaseMap.clear(); } } diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/bytecode_analysis/ForgeEventFactoryMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/bytecode_analysis/ForgeEventFactoryMixin.java new file mode 100644 index 00000000..b4e19754 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/faster_capabilities/bytecode_analysis/ForgeEventFactoryMixin.java @@ -0,0 +1,20 @@ +package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities.bytecode_analysis; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraftforge.event.ForgeEventFactory; +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.eventbus.api.IEventBus; +import org.embeddedt.modernfix.duck.IBatchingCapEvent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(value = ForgeEventFactory.class, remap = false) +public class ForgeEventFactoryMixin { + @WrapOperation(method = "gatherCapabilities(Lnet/minecraftforge/event/AttachCapabilitiesEvent;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;)Lnet/minecraftforge/common/capabilities/CapabilityDispatcher;", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/eventbus/api/IEventBus;post(Lnet/minecraftforge/eventbus/api/Event;)Z")) + private static boolean modernfix$sortAfterPost(IEventBus instance, Event event, Operation original) { + boolean result = original.call(instance, event); + ((IBatchingCapEvent) event).mfix$sortCaps(); + return result; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/duck/IBatchingCapEvent.java b/src/main/java/org/embeddedt/modernfix/duck/IBatchingCapEvent.java new file mode 100644 index 00000000..4efa3ff3 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/IBatchingCapEvent.java @@ -0,0 +1,5 @@ +package org.embeddedt.modernfix.duck; + +public interface IBatchingCapEvent { + void mfix$sortCaps(); +} From 2081b63b56857f4d5107cd8d92e812b547b1d7b3 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:38:18 -0400 Subject: [PATCH 03/10] Fix looking up `private static final` Capability fields --- ...CapabilityProviderDispatcherGenerator.java | 152 +++++++++++++++--- 1 file changed, 134 insertions(+), 18 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 72a10cad..6191a2d7 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -1,5 +1,6 @@ package org.embeddedt.modernfix.forge.capability; +import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.ICapabilityProvider; import net.minecraftforge.common.util.LazyOptional; import org.embeddedt.modernfix.ModernFix; @@ -10,6 +11,7 @@ import org.objectweb.asm.*; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; +import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -74,6 +76,7 @@ public class CapabilityProviderDispatcherGenerator { 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;>;"; + private static final String LOOKUP_DESC = "Ljava/lang/invoke/MethodHandles$Lookup;"; /** * Gets or generates a constructor MethodHandle for the given capability provider types. @@ -117,6 +120,14 @@ public class CapabilityProviderDispatcherGenerator { int generatedClassId = classCounter.incrementAndGet(); String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + generatedClassId; + List dispatches = optimizeDispatches(buildDispatchList(providerTypes, analysisResults)); + + // Assign a stable index to every unique CapabilityRef across all dispatches. + // We resolve the actual Capability instances here (in Java) so the generated + // only needs simple classDataAt calls - no reflection bytecode needed. + LinkedHashMap capRefIndices = collectCapabilityRefs(dispatches); + List> capValues = resolveCapabilityValues(capRefIndices); + ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> { StringBuilder sb = new StringBuilder(); for (int i = 0; i < providerTypes.size(); i++) { @@ -126,11 +137,14 @@ public class CapabilityProviderDispatcherGenerator { return sb; }); - byte[] classBytes = generateClassBytes(className, providerTypes, analysisResults); + byte[] classBytes = generateClassBytes(className, providerTypes, dispatches, capRefIndices); - // Define the hidden class - MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass( + // Define the hidden class, injecting the resolved Capability instances as class data. + // The generated retrieves them via MethodHandles.classDataAt so it never + // needs to perform reflection itself - private fields are handled transparently here. + MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClassWithClassData( classBytes, + capValues, true, MethodHandles.Lookup.ClassOption.NESTMATE ); @@ -154,6 +168,47 @@ public class CapabilityProviderDispatcherGenerator { } } + /** + * Collects all unique {@link CapabilityRef}s referenced by {@code dispatches} in encounter order, + * assigning each a stable list index for use with {@code classDataAt}. + */ + private static LinkedHashMap collectCapabilityRefs(List dispatches) { + LinkedHashMap result = new LinkedHashMap<>(); + for (ProviderDispatch dispatch : dispatches) { + if (dispatch instanceof ProviderDispatch.Guarded g) { + result.putIfAbsent(g.capability(), result.size()); + } else if (dispatch instanceof ProviderDispatch.Hash hash) { + for (ProviderDispatch.Guarded g : hash.entries()) { + result.putIfAbsent(g.capability(), result.size()); + } + } + } + return result; + } + + /** + * Resolves the actual {@link Capability} instances for all refs at class-generation time. + * Uses reflection (with {@code setAccessible}) so private fields are handled without any + * reflection bytecode appearing in the generated class. + */ + private static List> resolveCapabilityValues(LinkedHashMap capRefIndices) { + @SuppressWarnings("unchecked") + Capability[] caps = new Capability[capRefIndices.size()]; + for (Map.Entry entry : capRefIndices.entrySet()) { + CapabilityRef ref = entry.getKey(); + try { + Class clazz = Class.forName(ref.owner().replace('/', '.'), false, + CapabilityProviderDispatcherGenerator.class.getClassLoader()); + Field field = clazz.getDeclaredField(ref.fieldName()); + field.setAccessible(true); + caps[entry.getValue()] = (Capability) field.get(null); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to resolve capability field " + ref, e); + } + } + return Arrays.asList(caps); + } + /** * Build the dispatch list describing how each provider should be handled. */ @@ -261,9 +316,8 @@ public class CapabilityProviderDispatcherGenerator { return fields; } - private static byte[] generateClassBytes(String className, List> providerTypes, List analysisResults) { - List dispatches = optimizeDispatches(buildDispatchList(providerTypes, analysisResults)); - + private static byte[] generateClassBytes(String className, List> providerTypes, + List dispatches, LinkedHashMap capRefIndices) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) { @Override protected ClassLoader getClassLoader() { @@ -271,11 +325,13 @@ public class CapabilityProviderDispatcherGenerator { } }; + String internalName = className.replace('.', '/'); + // Class declaration: implements ICapabilityProvider cw.visit( V17, ACC_PUBLIC | ACC_FINAL | ACC_SUPER, - className.replace('.', '/'), + internalName, null, "java/lang/Object", new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" } @@ -301,17 +357,74 @@ public class CapabilityProviderDispatcherGenerator { } } + // Generate one static final field per unique CapabilityRef. + // These are populated in via MethodHandles.classDataAt, which reads the + // Capability instances injected by defineHiddenClassWithClassData. This avoids + // any reflection bytecode in the generated class and handles private fields transparently. + for (Map.Entry entry : capRefIndices.entrySet()) { + cw.visitField(ACC_PRIVATE | ACC_STATIC | ACC_FINAL, + capRefFieldName(entry.getValue()), CAPABILITY_DESC, null, null).visitEnd(); + } + + // Generate to load capability instances from class data + if (!capRefIndices.isEmpty()) { + generateClinit(cw, internalName, capRefIndices); + } + // Generate constructor - generateConstructor(cw, className, providerFields, dispatches); + generateConstructor(cw, className, providerFields, dispatches, capRefIndices); // Generate getCapability method with sided parameter - generateGetCapabilityMethod(cw, className, dispatches); + generateGetCapabilityMethod(cw, className, dispatches, capRefIndices); cw.visitEnd(); return cw.toByteArray(); } - private static void generateConstructor(ClassWriter cw, String className, Map providerFields, List dispatches) { + private static String capRefFieldName(int index) { + return "capRef" + index; + } + + /** + * Generates {@code } that loads each capability from class data injected at define time. + * The bytecode is simply: {@code capRefN = MethodHandles.classDataAt(lookup(), "", Capability.class, N)}. + */ + private static void generateClinit(ClassWriter cw, String internalName, LinkedHashMap capRefIndices) { + MethodVisitor mv = cw.visitMethod(ACC_STATIC, "", "()V", null, null); + mv.visitCode(); + + for (int i = 0; i < capRefIndices.size(); i++) { + // MethodHandles.lookup() + mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "lookup", + "()" + LOOKUP_DESC, false); + // "_" (classDataAt requires this exact name) + mv.visitLdcInsn("_"); + // Capability.class + mv.visitLdcInsn(Type.getType(CAPABILITY_DESC)); + // index + mv.visitLdcInsn(i); + // MethodHandles.classDataAt(lookup, name, type, index) → Object + mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "classDataAt", + "(" + LOOKUP_DESC + "Ljava/lang/String;Ljava/lang/Class;I)Ljava/lang/Object;", false); + mv.visitTypeInsn(CHECKCAST, "net/minecraftforge/common/capabilities/Capability"); + mv.visitFieldInsn(PUTSTATIC, internalName, capRefFieldName(i), CAPABILITY_DESC); + } + + mv.visitInsn(RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + /** + * Emits a load of the capability constant for {@code ref} from the generated class's own static field. + */ + private static void emitCapabilityLoad(MethodVisitor mv, String internalName, CapabilityRef ref, + Map capRefIndices) { + mv.visitFieldInsn(GETSTATIC, internalName, capRefFieldName(capRefIndices.get(ref)), CAPABILITY_DESC); + } + + private static void generateConstructor(ClassWriter cw, String className, Map providerFields, + List dispatches, Map capRefIndices) { 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('.', '/')); @@ -338,7 +451,7 @@ public class CapabilityProviderDispatcherGenerator { // Build hash maps for (ProviderDispatch dispatch : dispatches) { if (dispatch instanceof ProviderDispatch.Hash hash) { - generateMapConstruction(mg, classType, hash); + generateMapConstruction(mg, classType, hash, capRefIndices); } } @@ -346,7 +459,8 @@ public class CapabilityProviderDispatcherGenerator { mg.endMethod(); } - private static void generateMapConstruction(GeneratorAdapter mg, Type classType, ProviderDispatch.Hash hash) { + private static void generateMapConstruction(GeneratorAdapter mg, Type classType, ProviderDispatch.Hash hash, + Map capRefIndices) { List entries = hash.entries(); mg.loadThis(); // for PUTFIELD at the end @@ -356,7 +470,7 @@ public class CapabilityProviderDispatcherGenerator { ProviderDispatch.Guarded g = entries.get(i); mg.dup(); mg.push(i); - mg.visitFieldInsn(GETSTATIC, g.capability().owner(), g.capability().fieldName(), CAPABILITY_DESC); + emitCapabilityLoad(mg, classType.getInternalName(), g.capability(), capRefIndices); mg.loadArg(0); mg.push(g.providerIndex()); mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); @@ -370,7 +484,8 @@ public class CapabilityProviderDispatcherGenerator { mg.putField(classType, "capMap" + hash.mapIndex(), Type.getType(MAP_DESC)); } - private static void generateGetCapabilityMethod(ClassWriter cw, String className, List dispatches) { + private static void generateGetCapabilityMethod(ClassWriter cw, String className, List dispatches, + Map capRefIndices) { // Method: LazyOptional getCapability(Capability, Direction) MethodVisitor mv = cw.visitMethod( ACC_PUBLIC, @@ -401,7 +516,7 @@ public class CapabilityProviderDispatcherGenerator { emitHashDispatch(mv, internalName, getCapDesc, hash, nextLabel); di++; } else if (dispatch instanceof ProviderDispatch.Guarded) { - di = emitGuardedDispatch(mv, internalName, getCapDesc, dispatches, di, nextLabel); + di = emitGuardedDispatch(mv, internalName, getCapDesc, dispatches, di, nextLabel, capRefIndices); } else { var u = (ProviderDispatch.Unguarded) dispatch; emitProviderGetCapability(mv, internalName, getCapDesc, u.providerIndex(), u.fieldDesc()); @@ -490,7 +605,8 @@ public class CapabilityProviderDispatcherGenerator { * @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) { + List dispatches, int di, Label nextLabel, + Map capRefIndices) { var guarded = (ProviderDispatch.Guarded) dispatches.get(di); // Peek ahead to collect consecutive Guarded entries with same providerIndex @@ -507,7 +623,7 @@ public class CapabilityProviderDispatcherGenerator { var g = (ProviderDispatch.Guarded) dispatches.get(gi); CapabilityRef ref = g.capability(); mv.visitVarInsn(ALOAD, 1); - mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); + emitCapabilityLoad(mv, internalName, ref, capRefIndices); if (gi < groupEnd - 1) { mv.visitJumpInsn(IF_ACMPEQ, matchLabel); } else { @@ -544,4 +660,4 @@ public class CapabilityProviderDispatcherGenerator { } return result.toString(); } -} \ No newline at end of file +} From 8ee85f2c1637ea1ef365b066963aa24b2e1fdde9 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:31:24 -0400 Subject: [PATCH 04/10] Remove duplicate list held by DebugLevelSource --- .../DebugLevelSourceMixin.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_registry_alloc/DebugLevelSourceMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_registry_alloc/DebugLevelSourceMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_registry_alloc/DebugLevelSourceMixin.java new file mode 100644 index 00000000..1c295d1e --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_registry_alloc/DebugLevelSourceMixin.java @@ -0,0 +1,38 @@ +package org.embeddedt.modernfix.common.mixin.perf.forge_registry_alloc; + +import net.minecraft.world.level.levelgen.DebugLevelSource; +import net.minecraftforge.registries.GameData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.AbstractList; +import java.util.stream.Collector; +import java.util.stream.Stream; + +@Mixin(DebugLevelSource.class) +public class DebugLevelSourceMixin { + /** + * @author embeddedt + * @reason Reuse the existing blockstate list held by Forge instead of making a new one + */ + @Redirect(method = "initValidStates", at = @At(value = "INVOKE", target = "Ljava/util/stream/Stream;collect(Ljava/util/stream/Collector;)Ljava/lang/Object;", ordinal = 0), remap = false) + private static Object getStateList(Stream instance, Collector arCollector) { + var idMapper = GameData.getBlockStateIDMap(); + return new AbstractList<>() { + @Override + public int size() { + return idMapper.size(); + } + + @Override + public Object get(int index) { + var o = idMapper.byId(index); + if (o == null) { + throw new IndexOutOfBoundsException(); + } + return o; + } + }; + } +} From 5a9c49f8d405502c5c1e50a42cf27a8597e541a0 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:02:30 -0400 Subject: [PATCH 05/10] Add option to reduce memory usage of entity models --- .../CubeDefinitionMixin.java | 40 +++++++++++++++++++ .../core/config/ModernFixEarlyConfig.java | 9 +++-- 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/CubeDefinitionMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/CubeDefinitionMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/CubeDefinitionMixin.java new file mode 100644 index 00000000..29ee0411 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/CubeDefinitionMixin.java @@ -0,0 +1,40 @@ +package org.embeddedt.modernfix.common.mixin.perf.compact_entity_models; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.geom.builders.CubeDefinition; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Mixin(CubeDefinition.class) +@ClientOnlyMixin +public class CubeDefinitionMixin { + @Unique + private static final ConcurrentHashMap, ModelPart.Cube> MFIX_CUBE_CACHE = new ConcurrentHashMap<>(); + + /** + * @author embeddedt + * @reason deduplicate creation of Cube objects + */ + @WrapOperation(method = "bake", at = @At(value = "NEW", target = "(IIFFFFFFFFFZFFLjava/util/Set;)Lnet/minecraft/client/model/geom/ModelPart$Cube;")) + private ModelPart.Cube modernfix$deduplicateCube(int texCoordU, int texCoordV, float originX, float originY, float originZ, + float dimensionX, float dimensionY, float dimensionZ, float gtowX, + float growY, float growZ, boolean mirror, float texScaleU, + float texScaleV, Set visibleFaces, + Operation original) { + List cacheKey = List.of(texCoordU, texCoordV, originX, originY, originZ, dimensionX, dimensionY, dimensionZ, gtowX, growY, growZ, mirror, texScaleU, texScaleV, visibleFaces); + var cube = MFIX_CUBE_CACHE.get(cacheKey); + if (cube == null) { + cube = original.call((Object[])cacheKey.toArray()); + MFIX_CUBE_CACHE.put(cacheKey, cube); + } + return cube; + } +} 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 1b31f3b5..6b8473e5 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -174,15 +174,12 @@ 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) .put("mixin.feature.integrated_server_watchdog", true) .put("mixin.perf.faster_item_rendering", false) - .put("mixin.perf.ingredient_item_deduplication", false) .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) @@ -195,7 +192,11 @@ public class ModernFixEarlyConfig { .putConditionally(() -> !isFabric, "mixin.bugfix.fix_config_crashes", true) .putConditionally(() -> !isFabric, "mixin.bugfix.forge_at_inject_error", true) .putConditionally(() -> !isFabric, "mixin.feature.registry_event_progress", false) - .putConditionally(() -> isFabric, "mixin.perf.clear_fabric_mapping_tables", false) + // Beta (promote on next release) + .put("mixin.perf.compact_entity_models", false) + .put("mixin.perf.faster_capabilities.bytecode_analysis", false) + .put("mixin.perf.ingredient_item_deduplication", false) + // END .build(); private ModernFixEarlyConfig(File file) { From db13f39b30ca32deb62f516e8d6c434523f85ae8 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:53:51 -0400 Subject: [PATCH 06/10] Implement dynamic language loading --- .../ClientLanguageMixin.java | 57 +++++++++++++++++++ .../core/config/ModernFixEarlyConfig.java | 1 + .../dynamiclanguages/DynamicLanguageMap.java | 42 ++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java new file mode 100644 index 00000000..01595bdc --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java @@ -0,0 +1,57 @@ +package org.embeddedt.modernfix.common.mixin.perf.dynamic_languages; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.client.resources.language.ClientLanguage; +import net.minecraft.server.packs.resources.Resource; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.dynamiclanguages.DynamicLanguageMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +/** + * Modifies the language system to load/unload the contents of language entries based on GC pressure. + */ +@Mixin(ClientLanguage.class) +@ClientOnlyMixin +public class ClientLanguageMixin { + private static final ThreadLocal MFIX_MODIFY_APPEND_SEMANTICS = ThreadLocal.withInitial(() -> Boolean.FALSE); + + /** + * @author embeddedt + * @reason modify the semantics of appendFrom so that it's used to do a prepass + */ + @ModifyArg(method = "appendFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/locale/Language;loadFromJson(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V"), index = 1) + private static BiConsumer changeSemanticsOfConsumer(BiConsumer consumer, @Local(ordinal = 0, argsOnly = true) Map destinationMap, @Local(ordinal = 0) Resource resource) { + return MFIX_MODIFY_APPEND_SEMANTICS.get() ? ((k, v) -> destinationMap.put(k, resource)) : consumer; + } + + /** + * @author embeddedt + * @reason collect resources that own keys with a prepass + */ + @WrapOperation(method = "loadFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/language/ClientLanguage;appendFrom(Ljava/lang/String;Ljava/util/List;Ljava/util/Map;)V")) + private static void trackEntrySource(String languageName, List resources, Map destinationMap, Operation original) { + MFIX_MODIFY_APPEND_SEMANTICS.set(true); + try { + original.call(languageName, resources, destinationMap); + } finally { + MFIX_MODIFY_APPEND_SEMANTICS.remove(); + } + } + + /** + * @author embeddedt + * @reason figure out which keys are dynamically loaded and which are injected by mixins + */ + @ModifyArg(method = "loadFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/language/ClientLanguage;(Ljava/util/Map;Z)V"), index = 0) + private static Map modifyLanguageMap(Map storage) { + return DynamicLanguageMap.forStorage(Map.copyOf(storage)); + } +} 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 6b8473e5..32519749 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -194,6 +194,7 @@ public class ModernFixEarlyConfig { .putConditionally(() -> !isFabric, "mixin.feature.registry_event_progress", false) // Beta (promote on next release) .put("mixin.perf.compact_entity_models", false) + .put("mixin.perf.dynamic_languages", false) .put("mixin.perf.faster_capabilities.bytecode_analysis", false) .put("mixin.perf.ingredient_item_deduplication", false) // END diff --git a/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java b/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java new file mode 100644 index 00000000..a3471627 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java @@ -0,0 +1,42 @@ +package org.embeddedt.modernfix.dynamiclanguages; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Maps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.locale.Language; +import net.minecraft.server.packs.resources.Resource; +import org.embeddedt.modernfix.ModernFix; + +import java.io.IOException; +import java.util.Map; + +public class DynamicLanguageMap { + public static Map forStorage(Map storage) { + LoadingCache> languageFileContents = CacheBuilder.newBuilder() + .softValues() + .build(new CacheLoader<>() { + @Override + public Map load(Resource resource) throws Exception { + Map data = new Object2ObjectOpenHashMap<>(); + try (var stream = resource.open()) { + Language.loadFromJson(stream, data::put); + } catch (IOException e) { + ModernFix.LOGGER.error("Error loading language data from {}", resource.sourcePackId(), e); + } + return data; + } + }); + return Maps.asMap(storage.keySet(), k -> { + var value = storage.get(k); + if (value instanceof Resource r) { + return languageFileContents.getUnchecked(r).getOrDefault(k, ""); + } else if (value instanceof String s) { + return s; + } else { + return null; + } + }); + } +} From 4ff7d4c5549f80a5f0ed23262771b3c1a55ceb5d Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:43:09 -0400 Subject: [PATCH 07/10] Allow a single low-priority worker thread when cause_lag_by_disabling_threads is enabled On a system with few cores, we should still benefit from using one low-priority background thread for worldgen, because it avoids the server thread stopping to handle it itself. The thread will be blocked from progressing while higher-priority work (e.g. rendering or server ticking) is in progress. --- .../UtilMixin.java | 4 +- .../modernfix/util/DirectExecutorService.java | 43 ------------- .../util/SingleThreadedWorkerService.java | 63 +++++++++++++++++++ 3 files changed, 65 insertions(+), 45 deletions(-) delete mode 100644 src/main/java/org/embeddedt/modernfix/util/DirectExecutorService.java create mode 100644 src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/feature/cause_lag_by_disabling_threads/UtilMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/feature/cause_lag_by_disabling_threads/UtilMixin.java index a8b452bd..a9d494f4 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/feature/cause_lag_by_disabling_threads/UtilMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/feature/cause_lag_by_disabling_threads/UtilMixin.java @@ -1,7 +1,7 @@ package org.embeddedt.modernfix.common.mixin.feature.cause_lag_by_disabling_threads; import net.minecraft.Util; -import org.embeddedt.modernfix.util.DirectExecutorService; +import org.embeddedt.modernfix.util.SingleThreadedWorkerService; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mutable; @@ -12,5 +12,5 @@ import java.util.concurrent.ExecutorService; @Mixin(Util.class) public class UtilMixin { @Shadow @Final @Mutable - private static final ExecutorService BACKGROUND_EXECUTOR = new DirectExecutorService(); + private static final ExecutorService BACKGROUND_EXECUTOR = new SingleThreadedWorkerService(); } diff --git a/src/main/java/org/embeddedt/modernfix/util/DirectExecutorService.java b/src/main/java/org/embeddedt/modernfix/util/DirectExecutorService.java deleted file mode 100644 index 83b5d0aa..00000000 --- a/src/main/java/org/embeddedt/modernfix/util/DirectExecutorService.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.embeddedt.modernfix.util; - -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.concurrent.AbstractExecutorService; -import java.util.concurrent.TimeUnit; - -public class DirectExecutorService extends AbstractExecutorService { - private boolean isShutdown; - - @Override - public void shutdown() { - isShutdown = true; - } - - @NotNull - @Override - public List shutdownNow() { - isShutdown = true; - return List.of(); - } - - @Override - public boolean isShutdown() { - return isShutdown; - } - - @Override - public boolean isTerminated() { - return isShutdown; - } - - @Override - public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException { - return true; - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } -} diff --git a/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java b/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java new file mode 100644 index 00000000..ebc57823 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java @@ -0,0 +1,63 @@ +package org.embeddedt.modernfix.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Like {@link Executors#newSingleThreadExecutor()}, but handles the case where the background executor schedules + * a task to itself and waits for it the way a direct executor would. + */ +public class SingleThreadedWorkerService extends AbstractExecutorService { + private final AtomicReference thread = new AtomicReference<>(); + private final ExecutorService executorService; + + public SingleThreadedWorkerService() { + this.executorService = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "Worker-Main"); + t.setPriority(Thread.MIN_PRIORITY); + thread.set(t); + return t; + }); + } + + @Override + public void shutdown() { + executorService.shutdown(); + } + + @NotNull + @Override + public List shutdownNow() { + return executorService.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return executorService.isShutdown(); + } + + @Override + public boolean isTerminated() { + return executorService.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException { + return executorService.awaitTermination(timeout, unit); + } + + @Override + public void execute(@NotNull Runnable command) { + if (Thread.currentThread() == thread.get()) { + command.run(); + } else { + executorService.execute(command); + } + } +} From dc3c3790494d59775b25705eeeac4df67dfdac4d Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:45:59 -0400 Subject: [PATCH 08/10] Fix ChunkBiomeLookup leaking a worldgen region --- .../java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java index 8c368c92..03c9b6bb 100644 --- a/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java +++ b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java @@ -99,6 +99,7 @@ public class ChunkBiomeLookup implements Function> { public void dispose() { // Make sure we do not retain strong references to the biome holders Arrays.fill(biomes, null); + this.fallbackManager = null; } private boolean fetchBiomes(BiomeManager.NoiseBiomeSource source) { From 36f425b8cdc0c9f759f665abeddfc2881f1f6e05 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:07:59 -0400 Subject: [PATCH 09/10] Fix excessive recursion from mailbox --- .../modernfix/util/SingleThreadedWorkerService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java b/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java index ebc57823..1682ece6 100644 --- a/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java +++ b/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java @@ -1,5 +1,6 @@ package org.embeddedt.modernfix.util; +import net.minecraft.util.thread.ProcessorMailbox; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -52,9 +53,13 @@ public class SingleThreadedWorkerService extends AbstractExecutorService { return executorService.awaitTermination(timeout, unit); } + private static boolean isForcedAsyncCommand(Runnable command) { + return command instanceof ProcessorMailbox; + } + @Override public void execute(@NotNull Runnable command) { - if (Thread.currentThread() == thread.get()) { + if (!isForcedAsyncCommand(command) && Thread.currentThread() == thread.get()) { command.run(); } else { executorService.execute(command); From d08da1b3c88187facb994f1cdfe1b9fe7175e505 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:46:04 -0400 Subject: [PATCH 10/10] Disable release_protochunks when C2ME is installed --- .../embeddedt/modernfix/core/config/ModernFixEarlyConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 32519749..3743d065 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -241,7 +241,7 @@ public class ModernFixEarlyConfig { disableIfModPresent("mixin.bugfix.item_cache_flag", "lithium", "canary", "radium"); // DimThread makes changes to the server chunk manager (understandably), C2ME probably does the same disableIfModPresent("mixin.bugfix.chunk_deadlock", "c2me", "dimthread"); - disableIfModPresent("mixin.perf.reuse_datapacks", "tac"); + disableIfModPresent("mixin.perf.release_protochunks", "c2me"); disableIfModPresent("mixin.launch.class_search_cache", "optifine"); disableIfModPresent("mixin.perf.faster_texture_stitching", "optifine"); disableIfModPresent("mixin.bugfix.entity_pose_stack", "optifine");