diff --git a/src/main/java/org/embeddedt/modernfix/ModernFix.java b/src/main/java/org/embeddedt/modernfix/ModernFix.java index fbc0614a..14dddb81 100644 --- a/src/main/java/org/embeddedt/modernfix/ModernFix.java +++ b/src/main/java/org/embeddedt/modernfix/ModernFix.java @@ -2,9 +2,11 @@ package org.embeddedt.modernfix; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.server.ServerStartedEvent; @@ -21,6 +23,8 @@ import net.minecraftforge.fml.loading.FMLConfig; import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; import net.minecraftforge.network.NetworkConstants; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegisterEvent; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -99,6 +103,7 @@ public class ModernFix { MinecraftForge.EVENT_BUS.register(this); FMLJavaModLoadingContext.get().getModEventBus().addListener(this::commonSetup); FMLJavaModLoadingContext.get().getModEventBus().addListener(this::onLoadComplete); + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::registerItems); DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> MinecraftForge.EVENT_BUS.register(new ModernFixClient())); ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> NetworkConstants.IGNORESERVERONLY, (a, b) -> true)); ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, ModernFixConfig.COMMON_CONFIG); @@ -108,6 +113,17 @@ public class ModernFix { ModFileScanDataDeduplicator.deduplicate(); } + private void registerItems(RegisterEvent event) { + if(Boolean.getBoolean("modernfix.largeRegistryTest")) { + event.register(ForgeRegistries.Keys.ITEMS, helper -> { + Item.Properties props = new Item.Properties(); + for(int i = 0; i < 1000000; i++) { + helper.register(new ResourceLocation("modernfix", "item_" + i), new Item(props)); + } + }); + } + } + private static boolean dfuModPresent() { if(FMLConfig.isOptimizedDFUDisabled()) return true; diff --git a/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelLocationCache.java b/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelLocationCache.java index 25886e20..460a6da0 100644 --- a/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelLocationCache.java +++ b/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelLocationCache.java @@ -10,6 +10,7 @@ import net.minecraft.Util; import net.minecraft.client.renderer.block.BlockModelShaper; import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.core.Registry; +import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; @@ -20,7 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class ModelLocationCache { - private static final LoadingCache locationCache = CacheBuilder.newBuilder() + private static final LoadingCache blockLocationCache = CacheBuilder.newBuilder() .maximumSize(10000) .build(new CacheLoader() { @Override @@ -29,9 +30,26 @@ public class ModelLocationCache { } }); + private static final LoadingCache itemLocationCache = CacheBuilder.newBuilder() + .maximumSize(10000) + .build(new CacheLoader() { + @Override + public ModelResourceLocation load(Item key) throws Exception { + return new ModelResourceLocation(Registry.ITEM.getKey(key), "inventory"); + } + }); + public static ModelResourceLocation get(BlockState state) { try { - return locationCache.get(state); + return blockLocationCache.get(state); + } catch(ExecutionException e) { + throw new RuntimeException(e.getCause()); + } + } + + public static ModelResourceLocation get(Item item) { + try { + return itemLocationCache.get(item); } catch(ExecutionException e) { throw new RuntimeException(e.getCause()); } diff --git a/src/main/java/org/embeddedt/modernfix/mixin/devenv/GameDataMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/devenv/GameDataMixin.java new file mode 100644 index 00000000..34f531a3 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/devenv/GameDataMixin.java @@ -0,0 +1,16 @@ +package org.embeddedt.modernfix.mixin.devenv; + +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.registries.ForgeRegistry; +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; + +@Mixin(GameData.class) +public class GameDataMixin { + + @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/registries/ForgeRegistry;dump(Lnet/minecraft/resources/ResourceLocation;)V")) + private static void noDump(ForgeRegistry reg, ResourceLocation id) { + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemModelShaperMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemModelShaperMixin.java index 987b352b..3d372b1d 100644 --- a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemModelShaperMixin.java +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemModelShaperMixin.java @@ -8,6 +8,7 @@ import net.minecraft.core.Holder; import net.minecraft.world.item.Item; import net.minecraftforge.client.model.ForgeItemModelShaper; import net.minecraftforge.registries.ForgeRegistries; +import org.embeddedt.modernfix.dynamicresources.ModelLocationCache; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; @@ -23,6 +24,8 @@ public abstract class ItemModelShaperMixin extends ItemModelShaper { super(arg); } + private static final ModelResourceLocation SENTINEL = new ModelResourceLocation("modernfix", "sentinel"); + /** * @reason Get the stored location for that item and meta, and get the model * from that location from the model manager. @@ -30,7 +33,11 @@ public abstract class ItemModelShaperMixin extends ItemModelShaper { @Overwrite @Override public BakedModel getItemModel(Item item) { - ModelResourceLocation map = locations.get(ForgeRegistries.ITEMS.getDelegateOrThrow(item)); + ModelResourceLocation map = locations.getOrDefault(ForgeRegistries.ITEMS.getDelegateOrThrow(item), SENTINEL); + if(map == SENTINEL) { + /* generate the appropriate location from our cache */ + map = ModelLocationCache.get(item); + } return map == null ? null : getModelManager().getModel(map); } diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemRendererMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemRendererMixin.java new file mode 100644 index 00000000..3e47df2d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ItemRendererMixin.java @@ -0,0 +1,20 @@ +package org.embeddedt.modernfix.mixin.perf.dynamic_resources; + +import net.minecraft.client.renderer.ItemModelShaper; +import net.minecraft.client.renderer.entity.ItemRenderer; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.world.item.Item; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ItemRenderer.class) +public class ItemRendererMixin { + /** + * Don't waste space putting all these locations into the cache, compute them on demand later. + */ + @Redirect(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/ItemModelShaper;register(Lnet/minecraft/world/item/Item;Lnet/minecraft/client/resources/model/ModelResourceLocation;)V")) + private void skipDefaultRegistration(ItemModelShaper shaper, Item item, ModelResourceLocation mrl) { + + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ModelBakeryMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ModelBakeryMixin.java index 6734e139..f2736524 100644 --- a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ModelBakeryMixin.java +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ModelBakeryMixin.java @@ -11,6 +11,8 @@ import com.google.common.collect.Sets; import com.google.gson.*; import com.mojang.datafixers.util.Pair; import com.mojang.math.Transformation; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.minecraft.Util; import net.minecraft.client.color.block.BlockColors; import net.minecraft.client.renderer.block.model.BlockModel; @@ -19,8 +21,11 @@ import net.minecraft.client.renderer.texture.AtlasSet; import net.minecraft.client.renderer.texture.TextureAtlas; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.resources.ClientPackSource; import net.minecraft.client.resources.model.*; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.*; +import net.minecraft.server.packs.resources.FallbackResourceManager; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.profiling.ProfilerFiller; @@ -35,6 +40,8 @@ import net.minecraftforge.client.model.geometry.GeometryLoaderManager; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.ModLoader; import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.resource.DelegatingPackResources; +import net.minecraftforge.resource.PathPackResources; import org.apache.commons.lang3.tuple.Triple; import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.duck.IExtendedModelBakery; @@ -57,6 +64,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -196,12 +204,84 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { return ImmutableList.of(); } + private static final int ERROR_THRESHOLD = 200; + + private void logOrSuppressError(Object2IntOpenHashMap suppressionMap, String type, ResourceLocation location, Throwable e) { + int numErrors; + synchronized (suppressionMap) { + numErrors = suppressionMap.computeInt(location.getNamespace(), (k, oldVal) -> (oldVal == null ? 1 : oldVal + 1)); + } + if(numErrors <= ERROR_THRESHOLD) + ModernFix.LOGGER.error("Error reading {} {}: {}", type, location, e); + } + + private boolean trustedResourcePack(PackResources pack) { + return pack instanceof VanillaPackResources || + pack instanceof PathPackResources || + pack instanceof ClientPackSource || + pack instanceof DelegatingPackResources || + pack instanceof FolderPackResources || + pack instanceof FilePackResources; + } + + private void gatherAdditionalViaManualScan(List untrustedPacks, Set knownLocations, + Collection uncertainLocations, String filePrefix) { + if(untrustedPacks.size() > 0) { + /* Now make a fallback resource manager and use it on the remaining packs to see if they actually contain these files */ + FallbackResourceManager frm = new FallbackResourceManager(PackType.CLIENT_RESOURCES, "dummy"); + for (int i = untrustedPacks.size() - 1; i >= 0; i--) { + frm.push(untrustedPacks.get(i)); + } + for (ResourceLocation blockstate : uncertainLocations) { + if (knownLocations.contains(blockstate)) + continue; // don't check ones we know exist + ResourceLocation fileLocation = new ResourceLocation(blockstate.getNamespace(), filePrefix + blockstate.getPath() + ".json"); + Optional resource = frm.getResource(fileLocation); + if(resource.isPresent()) + knownLocations.add(blockstate); + } + } + } + /** * Load all blockstate JSONs and model files, collect textures. */ private void gatherModelMaterials(Set materialSet) { Stopwatch stopwatch = Stopwatch.createStarted(); List>> blockStateData = new ArrayList<>(); + final Object2IntOpenHashMap blockstateErrors = new Object2IntOpenHashMap<>(); + /* + * First, gather all vanilla packs, and use listResources on them. This will allow us to (hopefully) avoid + * scanning most packs a lot. + */ + List allPackResources = new ArrayList<>(this.resourceManager.listPacks().collect(Collectors.toList())); + Collections.reverse(allPackResources); + ObjectOpenHashSet allAvailableModels = new ObjectOpenHashSet<>(), allAvailableStates = new ObjectOpenHashSet<>(); + allPackResources.removeIf(pack -> { + if(trustedResourcePack(pack)) { + for(String namespace : pack.getNamespaces(PackType.CLIENT_RESOURCES)) { + Collection allBlockstates = pack.getResources(PackType.CLIENT_RESOURCES, namespace, "blockstates", p -> p.getPath().endsWith(".json")); + for(ResourceLocation blockstate : allBlockstates) { + allAvailableStates.add(new ResourceLocation(blockstate.getNamespace(), blockstate.getPath().replace("blockstates/", "").replace(".json", ""))); + } + Collection allModels = pack.getResources(PackType.CLIENT_RESOURCES, namespace, "models", p -> p.getPath().endsWith(".json")); + for(ResourceLocation blockstate : allModels) { + allAvailableModels.add(new ResourceLocation(blockstate.getNamespace(), blockstate.getPath().replace("models/", "").replace(".json", ""))); + } + } + return true; + } + ModernFix.LOGGER.debug("Pack with class {} needs manual scan", pack.getClass().getName()); + return false; + }); + + gatherAdditionalViaManualScan(allPackResources, allAvailableStates, blockStateFiles, "blockstates/"); + // We now have a list of all blockstates known to exist. Delete anything that we don't have + blockStateFiles.retainAll(allAvailableStates); + allAvailableStates.clear(); + allAvailableStates.trim(); + + for(ResourceLocation blockstate : blockStateFiles) { blockStateData.add(CompletableFuture.supplyAsync(() -> { ResourceLocation fileLocation = new ResourceLocation(blockstate.getNamespace(), "blockstates/" + blockstate.getPath() + ".json"); @@ -211,7 +291,7 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { JsonParser parser = new JsonParser(); return Pair.of(blockstate, parser.parse(new InputStreamReader(stream, StandardCharsets.UTF_8))); } catch(IOException | JsonParseException e) { - ModernFix.LOGGER.error("Error reading blockstate {}: {}", blockstate, e); + logOrSuppressError(blockstateErrors, "blockstate", blockstate, e); } } return Pair.of(blockstate, null); @@ -261,12 +341,25 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { } } catch(RuntimeException e) { - ModernFix.LOGGER.error("Error with blockstate {}: {}", pair.getFirst(), e); + logOrSuppressError(blockstateErrors, "blockstate", pair.getFirst(), e); } } } + blockstateErrors.object2IntEntrySet().forEach(entry -> { + if(entry.getIntValue() > ERROR_THRESHOLD) { + ModernFix.LOGGER.error("Suppressed additional {} blockstate errors for domain {}", entry.getIntValue(), entry.getKey()); + } + }); + blockstateErrors.clear(); blockStateData = null; + + /* figure out which models we should actually load */ + gatherAdditionalViaManualScan(allPackResources, allAvailableModels, modelFiles, "models/"); + modelFiles.retainAll(allAvailableModels); + allAvailableModels.clear(); + allAvailableModels.trim(); + Map basicModels = new HashMap<>(); basicModels.put(MISSING_MODEL_LOCATION, (BlockModel)missingModel); basicModels.put(new ResourceLocation("builtin/generated"), GENERATION_MARKER); @@ -285,7 +378,7 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { JsonParser parser = new JsonParser(); return Pair.of(model, parser.parse(new InputStreamReader(stream, StandardCharsets.UTF_8))); } catch(IOException | JsonParseException e) { - ModernFix.LOGGER.error("Error reading model {}: {}", fileLocation, e); + logOrSuppressError(blockstateErrors, "model", fileLocation, e); } } return Pair.of(fileLocation, null); @@ -306,12 +399,17 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { continue; } } catch(Throwable e) { - ModernFix.LOGGER.warn("Unable to parse {}: {}", pair.getFirst(), e); + logOrSuppressError(blockstateErrors, "model", pair.getFirst(), e); } basicModels.put(pair.getFirst(), (BlockModel)missingModel); } UVController.useDummyUv.set(Boolean.FALSE); } + blockstateErrors.object2IntEntrySet().forEach(entry -> { + if(entry.getIntValue() > ERROR_THRESHOLD) { + ModernFix.LOGGER.error("Suppressed additional {} model errors for domain {}", entry.getIntValue(), entry.getKey()); + } + }); modelFiles = null; Function modelGetter = loc -> { UnbakedModel m = basicModels.get(loc); diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ctm/TextureMetadataHandlerMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ctm/TextureMetadataHandlerMixin.java index f00a5b91..882b53c7 100644 --- a/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ctm/TextureMetadataHandlerMixin.java +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/dynamic_resources/ctm/TextureMetadataHandlerMixin.java @@ -15,6 +15,7 @@ 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 team.chisel.ctm.CTM; import team.chisel.ctm.client.model.AbstractCTMBakedModel; @@ -36,6 +37,11 @@ public abstract class TextureMetadataHandlerMixin { MinecraftForge.EVENT_BUS.addListener(this::onDynamicModelBake); } + @Redirect(method = "onModelBake", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/BakedModel;isCustomRenderer()Z")) + private boolean checkModelValid(BakedModel model) { + return model == null || model.isCustomRenderer(); + } + public void onDynamicModelBake(DynamicModelBakeEvent event) { UnbakedModel rootModel = event.getUnbakedModel(); BakedModel baked = event.getModel(); diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistryMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistryMixin.java index 163e27f5..90df21e0 100644 --- a/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistryMixin.java +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistryMixin.java @@ -1,15 +1,29 @@ package org.embeddedt.modernfix.mixin.perf.fast_registry_validation; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; +import com.google.common.collect.BiMap; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.registries.ForgeRegistry; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.embeddedt.modernfix.registry.FastForgeRegistry; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.lang.reflect.Method; +import java.util.*; @Mixin(value = ForgeRegistry.class, remap = false) -public class ForgeRegistryMixin { +public class ForgeRegistryMixin { private static Method bitSetTrimMethod = null; private static boolean bitSetTrimMethodRetrieved = false; @@ -25,4 +39,71 @@ public class ForgeRegistryMixin { } return bitSetTrimMethod; } + + private int expectedNextBit = -1; + + /** + * Avoid calling nextClearBit and scanning the whole registry for every block registration. + */ + @Redirect(method = "add(ILnet/minecraft/resources/ResourceLocation;Ljava/lang/Object;Ljava/lang/String;)I", at = @At(value = "INVOKE", target = "Ljava/util/BitSet;nextClearBit(I)I")) + private int useCachedBit(BitSet availabilityMap, int minimum) { + int bit = availabilityMap.nextClearBit(expectedNextBit != -1 ? expectedNextBit : minimum); + expectedNextBit = bit + 1; + return bit; + } + + @Inject(method = { "sync", "clear", "block" }, at = @At("HEAD")) + private void clearBitCache(CallbackInfo ci) { + expectedNextBit = -1; + } + + @Inject(method = "createAndAddDummy", at = @At(value = "INVOKE", target = "Ljava/util/BitSet;clear(I)V")) + private void clearBitCache2(CallbackInfo ci) { + expectedNextBit = -1; + } + + @Redirect(method = "add(ILnet/minecraft/resources/ResourceLocation;Ljava/lang/Object;Ljava/lang/String;)I", at = @At(value = "INVOKE", target = "Lorg/apache/logging/log4j/Logger;trace(Lorg/apache/logging/log4j/Marker;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V")) + private void skipTrace(Logger logger, Marker marker, String s, Object o, Object o1, Object o2, Object o3, Object o4) { + + } + + @Shadow @Final @Mutable private BiMap ids; + + @Shadow @Final @Mutable private BiMap, V> keys; + + @Shadow @Final private ResourceKey> key; + + @Shadow @Final @Mutable private BiMap names; + + @Shadow @Final @Mutable private BiMap owners; + + private FastForgeRegistry fastRegistry; + + /** + * The following code replaces the Forge HashBiMaps with a more efficient data structure based around + * an array list for IDs and one HashMap going from value -> information. + */ + @Inject(method = "", at = @At("RETURN")) + private void replaceBackingMaps(CallbackInfo ci) { + this.fastRegistry = new FastForgeRegistry<>(this.key); + this.ids = fastRegistry.getIds(); + this.keys = fastRegistry.getKeys(); + this.names = fastRegistry.getNames(); + this.owners = fastRegistry.getOwners(); + } + + @Inject(method = "freeze", at = @At("RETURN")) + private void optimizeRegistry(CallbackInfo ci) { + this.fastRegistry.optimize(); + } + + @Redirect(method = "sync", at = @At(value = "INVOKE", target = "Lcom/google/common/collect/BiMap;clear()V")) + private void clearBiMap(BiMap map) { + if(map == this.owners) { + this.fastRegistry.clear(); + } else if(map == this.keys || map == this.names || map == this.ids) { + // do nothing, the registry is faster at clearing everything at once + } else + map.clear(); + } } diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistrySnapshotMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistrySnapshotMixin.java new file mode 100644 index 00000000..ad941ab0 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ForgeRegistrySnapshotMixin.java @@ -0,0 +1,33 @@ +package org.embeddedt.modernfix.mixin.perf.fast_registry_validation; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.registries.ForgeRegistry; +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; +import java.util.Set; + +@Mixin(ForgeRegistry.Snapshot.class) +public class ForgeRegistrySnapshotMixin { + @Shadow @Final @Mutable public Map ids; + + @Shadow @Final @Mutable public Set dummied; + + /** + * The only good reason to use tree maps here is to keep the order the same. But we are tracking IDs + * anyway so order shouldn't matter. We replace the maps that will be most used. + */ + @Inject(method = "", at = @At("RETURN")) + private void replaceSnapshotMaps(CallbackInfo ci) { + this.ids = new Object2ObjectOpenHashMap<>(); + this.dummied = new ObjectOpenHashSet<>(); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ResourceKeyMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ResourceKeyMixin.java new file mode 100644 index 00000000..804dce98 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/fast_registry_validation/ResourceKeyMixin.java @@ -0,0 +1,28 @@ +package org.embeddedt.modernfix.mixin.perf.fast_registry_validation; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +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 java.util.Map; + +@Mixin(ResourceKey.class) +public class ResourceKeyMixin { + private static Map>> INTERNING_MAP = new Object2ObjectOpenHashMap<>(); + @Inject(method = "create(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/resources/ResourceKey;", at = @At("HEAD"), cancellable = true) + private static void createEfficient(ResourceLocation parent, ResourceLocation location, CallbackInfoReturnable> cir) { + synchronized (ResourceKey.class) { + Map> keys = INTERNING_MAP.computeIfAbsent(parent, k -> new Object2ObjectOpenHashMap<>()); + ResourceKey key = keys.get(location); + if(key == null) { + key = new ResourceKey<>(parent, location); + keys.put(location, key); + } + cir.setReturnValue((ResourceKey)key); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/registry/FastForgeRegistry.java b/src/main/java/org/embeddedt/modernfix/registry/FastForgeRegistry.java new file mode 100644 index 00000000..67be3c21 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/registry/FastForgeRegistry.java @@ -0,0 +1,602 @@ +package org.embeddedt.modernfix.registry; + +import com.google.common.collect.BiMap; +import com.google.common.collect.Iterators; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class FastForgeRegistry { + private final BiMap ids; + private final DataFieldBiMap names; + private final DataFieldBiMap> keys; + private final DataFieldBiMap owners; + private final ResourceKey> registryKey; + + private final ObjectArrayList valuesById; + private final Object2ObjectOpenHashMap infoByValue; + + private void storeId(V value, int id) { + RegistryValueData pair = infoByValue.computeIfAbsent(value, k -> new RegistryValueData()); + pair.id = id; + } + + private void updateInfoPairAndClearIfNull(V v, Consumer consumer) { + infoByValue.compute(v, (oldValue, oldPair) -> { + if(oldPair == null) + oldPair = new RegistryValueData(); + consumer.accept(oldPair); + if(oldPair.isEmpty()) + return null; + else + return oldPair; + }); + } + + private void ensureArrayCanFitId(int id) { + int desiredSize = id + 1; + while(valuesById.size() < desiredSize) { + valuesById.add(null); + } + } + + public void clear() { + this.infoByValue.clear(); + for(int i = 0; i < this.valuesById.size(); i++) { + this.valuesById.set(i, null); + } + this.names.clearUnsafe(); + this.keys.clearUnsafe(); + this.owners.clearUnsafe(); + } + + public FastForgeRegistry(ResourceKey> registryKey) { + this.registryKey = registryKey; + this.valuesById = new ObjectArrayList<>(); + this.infoByValue = new Object2ObjectOpenHashMap<>(); + this.keys = new DataFieldBiMap<>(p -> (ResourceKey) p.key, (p, k) -> p.key = k); + this.owners = new DataFieldBiMap<>(p -> p.overrideOwner, (p, k) -> p.overrideOwner = k); + this.names = new DataFieldBiMap<>(p -> p.location, (p, l) -> p.location = l); + // IDs require a specialized implementation, as we back the K->V direction with an array + this.ids = new BiMap() { + @Nullable + @Override + public V put(@Nullable Integer key, @Nullable V value) { + RegistryValueData data = infoByValue.get(value); + int unboxedKey = key; + if(data != null && data.id != -1 && data.id != unboxedKey) + throw new IllegalArgumentException("Existing mapping for ID " + data.id + " value " + value + " when new ID " + unboxedKey + " was requested"); + ensureArrayCanFitId(unboxedKey); + V oldValue = valuesById.set(unboxedKey, value); + storeId(value, unboxedKey); + return oldValue; + } + + @Nullable + @Override + public V forcePut(@Nullable Integer key, @Nullable V value) { + ensureArrayCanFitId(key); + V oldValue = valuesById.set(key, value); + if(oldValue != null) { + updateInfoPairAndClearIfNull(oldValue, pair -> pair.id = -1); + } + storeId(value, key); + return oldValue; + } + + @Override + public void putAll(Map map) { + map.forEach(this::put); + } + + @Override + public Set values() { + return Collections.unmodifiableSet(infoByValue.keySet()); + } + + @Override + public BiMap inverse() { + return new BiMap() { + @Nullable + @Override + public Integer put(@Nullable V key, @Nullable Integer value) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public Integer forcePut(@Nullable V key, @Nullable Integer value) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map map) { + throw new UnsupportedOperationException(); + } + + @Override + public Set values() { + throw new UnsupportedOperationException(); + } + + @Override + public BiMap inverse() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsKey(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Integer get(Object key) { + RegistryValueData pair = infoByValue.get(key); + if(pair == null) + return null; + return pair.id == -1 ? null : pair.id; + } + + @Override + public Integer remove(Object key) { + RegistryValueData pair = infoByValue.get(key); + if(pair == null) + return null; + int id = pair.id; + if(id != -1) + valuesById.set(id, null); + updateInfoPairAndClearIfNull((V)key, p -> p.id = -1); + return id == -1 ? null : id; + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set keySet() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public int size() { + return infoByValue.size(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsKey(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public V get(Object key) { + int id = (Integer)key; + if(id < 0 || id >= valuesById.size()) + return null; + else + return valuesById.get(id); + } + + @Override + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + valuesById.clear(); + infoByValue.values().removeIf(pair -> { + pair.id = -1; + return pair.isEmpty(); + }); + } + + @NotNull + @Override + public Set keySet() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public void forEach(BiConsumer action) { + for(int i = 0 ; i < valuesById.size(); i++) { + V val = valuesById.get(i); + if(val != null) + action.accept(i, val); + } + } + }; + } + + public void optimize() { + this.keys.optimize(); + this.owners.optimize(); + this.names.optimize(); + this.infoByValue.trim(); + } + + public BiMap getIds() { + return ids; + } + + public BiMap, V> getKeys() { + return keys; + } + + public BiMap getNames() { + return names; + } + + public DataFieldBiMap getOwners() { + return owners; + } + + /** + * Custom BiMap implementation that uses one internal hash map for the K->V direction, and the shared global + * information hash map for the V->K direction. + */ + class DataFieldBiMap implements BiMap { + public final Object2ObjectOpenHashMap valuesByKey = new Object2ObjectOpenHashMap<>(); + private final Function getter; + private final BiConsumer setter; + + public DataFieldBiMap(Function getter, BiConsumer setter) { + this.getter = getter; + this.setter = setter; + } + + public void optimize() { + this.valuesByKey.trim(); + } + + public void clearUnsafe() { + this.valuesByKey.clear(); + } + + private V forcePut(@Nullable K key, @Nullable V value, boolean throwOnExisting) { + if(throwOnExisting) { + RegistryValueData dataForValue = infoByValue.get(value); + // null check could be wrong if null is a valid value but doesn't matter in Forge's use + if(dataForValue != null && getter.apply(dataForValue) != null && !Objects.equals(getter.apply(dataForValue), key)) { + throw new IllegalArgumentException("Existing mapping for key " + key + " value " + value); + } + } + V oldValue = valuesByKey.put(key, value); + if(oldValue != null) { + updateInfoPairAndClearIfNull(oldValue, p -> setter.accept(p, null)); + } + updateInfoPairAndClearIfNull(value, p -> setter.accept(p, key)); + return oldValue; + } + + @Nullable + @Override + public V put(@Nullable K key, @Nullable V value) { + return forcePut(key, value, true); + } + + @Nullable + @Override + public V forcePut(@Nullable K key, @Nullable V value) { + return forcePut(key, value, false); + } + + @Override + public void putAll(Map map) { + map.forEach(this::put); + } + + @Override + public Set values() { + return Collections.unmodifiableSet(infoByValue.keySet()); + } + + private DataFieldBiMapInverse inverse = null; + + @Override + public BiMap inverse() { + if(inverse == null) + inverse = new DataFieldBiMapInverse<>(this); + return inverse; + } + + @Override + public int size() { + return valuesByKey.size(); + } + + @Override + public boolean isEmpty() { + return valuesByKey.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return valuesByKey.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return infoByValue.containsKey(value); + } + + @Override + public V get(Object key) { + return valuesByKey.get(key); + } + + @Override + public V remove(Object key) { + V value = get(key); + if(value == null) + return null; + inverse().remove(value); + return value; + } + + @Override + public void clear() { + valuesByKey.values().forEach(v -> updateInfoPairAndClearIfNull(v, p -> p.key = null)); + valuesByKey.clear(); + } + + @NotNull + @Override + public Set keySet() { + return Collections.unmodifiableSet(valuesByKey.keySet()); + } + + @NotNull + @Override + public Set> entrySet() { + return Collections.unmodifiableSet(valuesByKey.entrySet()); + } + + + } + + class DataFieldBiMapInverse implements BiMap { + private final DataFieldBiMap forward; + + public DataFieldBiMapInverse(DataFieldBiMap forward) { + this.forward = forward; + } + + @Nullable + @Override + public K put(@Nullable V key, @Nullable K value) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public K forcePut(@Nullable V key, @Nullable K value) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map map) { + throw new UnsupportedOperationException(); + } + + @Override + public Set values() { + throw new UnsupportedOperationException(); + } + + @Override + public BiMap inverse() { + return forward; + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsKey(Object key) { + return infoByValue.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return forward.valuesByKey.containsKey(value); + } + + @Override + public K get(Object key) { + RegistryValueData pair = infoByValue.get(key); + if(pair == null) + return null; + else + return forward.getter.apply(pair); + } + + @Override + public K remove(Object key) { + RegistryValueData pair = infoByValue.get(key); + if(pair == null) + return null; + else { + K rk = forward.getter.apply(pair); + forward.valuesByKey.remove(rk); + updateInfoPairAndClearIfNull((V)key, p -> forward.setter.accept(p, null)); + return rk; + } + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set keySet() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + } + + class FastForgeRegistryLocationSet implements Set { + private final Set> backingSet; + + public FastForgeRegistryLocationSet(Set> backingSet) { + this.backingSet = backingSet; + } + + @Override + public int size() { + return backingSet.size(); + } + + @Override + public boolean isEmpty() { + return backingSet.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return backingSet.contains(ResourceKey.create(FastForgeRegistry.this.registryKey, (ResourceLocation)o)); + } + + @NotNull + @Override + public Iterator iterator() { + return Iterators.transform(backingSet.iterator(), ResourceKey::location); + } + + @NotNull + @Override + public Object[] toArray() { + Object[] keyArray = backingSet.toArray(); + for(int i = 0; i < keyArray.length; i++) { + keyArray[i] = ((ResourceKey)keyArray[i]).location(); + } + return keyArray; + } + + @NotNull + @Override + public T[] toArray(@NotNull T[] a) { + Object[] keyArray = backingSet.toArray(); + T[] finalArray = Arrays.copyOf(a, keyArray.length); + for(int i = 0; i < keyArray.length; i++) { + finalArray[i] = (T)((ResourceKey)keyArray[i]).location(); + } + return finalArray; + } + + @Override + public boolean add(ResourceLocation resourceLocation) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + for(Object o : c) { + if(!contains(o)) + return false; + } + return true; + } + + @Override + public boolean addAll(@NotNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + } + + static class RegistryValueData { + public ResourceKey key; + public ResourceLocation location; + public int id = -1; + public Object overrideOwner; + + boolean isEmpty() { + return key == null && location == null && id == -1 && overrideOwner == null; + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/util/CachedResourcePath.java b/src/main/java/org/embeddedt/modernfix/util/CachedResourcePath.java index 5b32b41f..889311ae 100644 --- a/src/main/java/org/embeddedt/modernfix/util/CachedResourcePath.java +++ b/src/main/java/org/embeddedt/modernfix/util/CachedResourcePath.java @@ -25,30 +25,30 @@ public class CachedResourcePath { private static final String[] NO_PREFIX = new String[0]; public CachedResourcePath(Path path) { - this(NO_PREFIX, path, path.getNameCount()); + this(NO_PREFIX, path, path.getNameCount(), true); } public CachedResourcePath(String s) { // normalize so we can guarantee there are no empty sections - this(NO_PREFIX, SLASH_SPLITTER.splitToList(FileUtil.normalize(s))); + this(NO_PREFIX, SLASH_SPLITTER.splitToList(FileUtil.normalize(s)), false); } - public CachedResourcePath(String[] prefixElements, Collection collection) { - this(prefixElements, collection, collection.size()); + public CachedResourcePath(String[] prefixElements, Collection collection, boolean intern) { + this(prefixElements, collection, collection.size(), intern); } - public CachedResourcePath(String[] prefixElements, Iterable path, int count) { + public CachedResourcePath(String[] prefixElements, Iterable path, int count, boolean intern) { String[] components = new String[prefixElements.length + count]; int i = 0; while(i < prefixElements.length) { - components[i] = PATH_COMPONENT_INTERNER.intern(prefixElements[i]); + components[i] = intern ? PATH_COMPONENT_INTERNER.intern(prefixElements[i]) : prefixElements[i]; i++; } for(Object component : path) { String s = component.toString(); if(s.length() == 0) continue; - components[i] = PATH_COMPONENT_INTERNER.intern(s); + components[i] = intern ? PATH_COMPONENT_INTERNER.intern(s) : s; i++; } pathComponents = components; diff --git a/src/main/java/org/embeddedt/modernfix/util/CanonizingStringMap.java b/src/main/java/org/embeddedt/modernfix/util/CanonizingStringMap.java index 737e9bca..e5bb2f2d 100644 --- a/src/main/java/org/embeddedt/modernfix/util/CanonizingStringMap.java +++ b/src/main/java/org/embeddedt/modernfix/util/CanonizingStringMap.java @@ -98,7 +98,8 @@ public class CanonizingStringMap implements Map { @NotNull @Override public Set keySet() { - return Collections.unmodifiableSet(this.backingMap.keySet()); + // has to be modifiable because mods (cough, Tinkers) use it to clear the tag + return this.backingMap.keySet(); } @NotNull diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index ca563f66..e7b00b3b 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -28,4 +28,5 @@ public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor public net.minecraft.nbt.CompoundTag (Ljava/util/Map;)V # public net.minecraft.client.renderer.texture.TextureAtlasSprite (Lnet/minecraft/client/renderer/texture/TextureAtlas;Lnet/minecraft/client/renderer/texture/TextureAtlasSprite$Info;IIIIILcom/mojang/blaze3d/platform/NativeImage;)V # public net.minecraft.server.level.DistanceManager m_140792_(Lnet/minecraft/server/level/TicketType;Lnet/minecraft/world/level/ChunkPos;ILjava/lang/Object;)V # addTicket -public net.minecraft.server.level.ChunkMap$DistanceManager \ No newline at end of file +public net.minecraft.server.level.ChunkMap$DistanceManager +public net.minecraft.resources.ResourceKey (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V # diff --git a/src/main/resources/modernfix.mixins.json b/src/main/resources/modernfix.mixins.json index 84cf424d..b0a5ae0f 100644 --- a/src/main/resources/modernfix.mixins.json +++ b/src/main/resources/modernfix.mixins.json @@ -35,10 +35,12 @@ "feature.branding.BrandingControlMixin", "feature.direct_stack_trace.CrashReportMixin", "perf.nbt_memory_usage.CompoundTagMixin", - "perf.fast_registry_validation.ForgeRegistryMixin", "perf.kubejs.RecipeEventJSMixin", "perf.tag_id_caching.TagEntryMixin", "perf.tag_id_caching.TagOrElementLocationMixin", + "perf.fast_registry_validation.ForgeRegistryMixin", + "perf.fast_registry_validation.ForgeRegistrySnapshotMixin", + "perf.fast_registry_validation.ResourceKeyMixin", "perf.cache_strongholds.ChunkGeneratorMixin", "perf.cache_upgraded_structures.StructureManagerMixin", "perf.cache_strongholds.ServerLevelMixin", @@ -47,7 +49,8 @@ "perf.compress_blockstate.BlockBehaviourMixin", "perf.dedup_blockstate_flattening_map.BlockStateDataMixin", "perf.dedup_blockstate_flattening_map.ChunkPalettedStorageFixMixin", - "devenv.MinecraftServerMixin" + "devenv.MinecraftServerMixin", + "devenv.GameDataMixin" ], "client": [ "core.MinecraftMixin", @@ -61,6 +64,7 @@ "perf.dynamic_resources.BlockElementFaceDeserializerMixin", "perf.dynamic_resources.BlockModelShaperMixin", "perf.dynamic_resources.ItemModelShaperMixin", + "perf.dynamic_resources.ItemRendererMixin", "perf.dynamic_resources.ModelBakeryMixin", "perf.dynamic_resources.ae2.RegistrationMixin", "perf.dynamic_resources.ctm.TextureMetadataHandlerMixin",