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 1/4] 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 2/4] 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 3/4] 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 4/4] 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); }