From 992269034d9397b35b713cbb2a41c4367fe2d7df Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Mon, 8 May 2023 14:10:54 -0400 Subject: [PATCH] Rewrite Fabric dynamic model loader to not load all models at startup --- .../dynamicresources/ModelBakeryHelpers.java | 7 +- .../dynamic_resources/ModelBakeryMixin.java | 158 ++++++++++++++++-- 2 files changed, 152 insertions(+), 13 deletions(-) diff --git a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelBakeryHelpers.java b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelBakeryHelpers.java index e0df2a26..0f993e12 100644 --- a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelBakeryHelpers.java +++ b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelBakeryHelpers.java @@ -268,6 +268,7 @@ public class ModelBakeryHelpers { ModernFix.LOGGER.error("Suppressed additional {} model errors for domain {}", entry.getIntValue(), entry.getKey()); } }); + blockstateErrors.clear(); modelFiles = null; Function modelGetter = loc -> { UnbakedModel m = basicModels.get(loc); @@ -275,7 +276,11 @@ public class ModelBakeryHelpers { return m != null ? m : bakeryModelGetter.apply(loc); }; for(BlockModel model : basicModels.values()) { - materialSet.addAll(model.getMaterials(modelGetter, errorSet)); + try { + materialSet.addAll(model.getMaterials(modelGetter, errorSet)); + } catch(Throwable e) { + ModernFix.LOGGER.error("Model {} threw error while getting materials", model.name, e); + } } //errorSet.stream().filter(pair -> !pair.getSecond().equals(MISSING_MODEL_LOCATION_STRING)).forEach(pair -> LOGGER.warn("Unable to resolve texture reference: {} in {}", pair.getFirst(), pair.getSecond())); stopwatch.stop(); diff --git a/fabric/src/main/java/org/embeddedt/modernfix/fabric/mixin/perf/dynamic_resources/ModelBakeryMixin.java b/fabric/src/main/java/org/embeddedt/modernfix/fabric/mixin/perf/dynamic_resources/ModelBakeryMixin.java index 185bb732..6a47edba 100644 --- a/fabric/src/main/java/org/embeddedt/modernfix/fabric/mixin/perf/dynamic_resources/ModelBakeryMixin.java +++ b/fabric/src/main/java/org/embeddedt/modernfix/fabric/mixin/perf/dynamic_resources/ModelBakeryMixin.java @@ -1,23 +1,33 @@ package org.embeddedt.modernfix.fabric.mixin.perf.dynamic_resources; +import com.google.common.base.Stopwatch; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalNotification; import com.google.common.collect.ForwardingMap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.mojang.datafixers.util.Pair; import com.mojang.math.Transformation; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.minecraft.client.renderer.block.model.BlockModel; +import net.minecraft.client.renderer.block.model.BlockModelDefinition; import net.minecraft.client.renderer.block.model.ItemModelGenerator; import net.minecraft.client.renderer.block.model.MultiVariant; import net.minecraft.client.renderer.block.model.multipart.MultiPart; -import net.minecraft.client.renderer.texture.AtlasSet; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.renderer.texture.*; +import net.minecraft.client.resources.ClientPackSource; import net.minecraft.client.resources.model.*; +import net.minecraft.core.Registry; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.FilePackResources; +import net.minecraft.server.packs.FolderPackResources; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.VanillaPackResources; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import org.apache.commons.lang3.tuple.Triple; @@ -34,6 +44,7 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.*; @@ -41,6 +52,9 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; /* high priority so that our injectors are added before other mods' */ @Mixin(value = ModelBakery.class, priority = 600) @@ -75,6 +89,12 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { @Shadow @Nullable public abstract BakedModel bake(ResourceLocation location, ModelState transform); @Shadow @Final private Map topLevelModels; + @Shadow @Final private static String MISSING_MODEL_LOCATION_STRING; + + @Shadow protected abstract void cacheAndQueueDependencies(ResourceLocation location, UnbakedModel model); + + @Shadow @Final private BlockModelDefinition.Context context; + @Shadow @Final private static Map> STATIC_DEFINITIONS; private Cache, BakedModel> loadedBakedModels; private Cache loadedModels; @@ -141,6 +161,103 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { return this.missingModel; } + private Set blockStateFiles = new ObjectOpenHashSet<>(); + private Set modelFiles = new ObjectOpenHashSet<>(); + + private boolean forceLoadModel = false; + + @Inject(method = "loadModel", at = @At(value = "HEAD", shift = At.Shift.AFTER), cancellable = true) + private void ignoreNonFabricModel(ResourceLocation modelLocation, CallbackInfo ci) throws Exception { + if(this.inTextureGatheringPass && !this.forceLoadModel) { + // Custom model processor, try to avoid loading unwrapped models + // First add this to the list of models to scan for textures + ResourceLocation blockStateLocation = null; + if(modelLocation instanceof ModelResourceLocation) { + ModelResourceLocation location = (ModelResourceLocation)modelLocation; + if(Objects.equals(location.getVariant(), "inventory")) { + modelFiles.add(new ResourceLocation(location.getNamespace(), "item/" + location.getPath())); + } else { + blockStateLocation = new ResourceLocation(location.getNamespace(), location.getPath()); + blockStateFiles.add(blockStateLocation); + } + } else + modelFiles.add(modelLocation); + // Now check if it's a wrapped model + boolean isWrappedModel = false; + Set oldLoadingStack = this.loadingStack.size() > 0 ? new ObjectOpenHashSet<>(this.loadingStack) : ImmutableSet.of(); + // Set the correct blockstate context + StateDefinition statecontainer; + if(blockStateLocation != null) { + statecontainer = STATIC_DEFINITIONS.get(blockStateLocation); + if(statecontainer == null) + statecontainer = Registry.BLOCK.get(blockStateLocation).getStateDefinition(); + } else + statecontainer = Blocks.AIR.getStateDefinition(); + this.context.setDefinition(statecontainer); + // Pretend to be caching the model by caching the missing model, if the actually cached model isn't + // the exact same instance we know a mixin tampered with it + this.forceLoadModel = true; + this.cacheAndQueueDependencies(modelLocation, this.missingModel); + this.forceLoadModel = false; + this.loadingStack.clear(); + this.loadingStack.addAll(oldLoadingStack); + if(this.smallLoadingCache.get(modelLocation) != this.missingModel) { + /* probably a wrapped model, allow it to load normally */ + isWrappedModel = true; + } + this.smallLoadingCache.clear(); + this.unbakedCache.remove(modelLocation); + // Load the model through the normal code path + if(isWrappedModel) { + ModernFix.LOGGER.warn("Model {} appears to be replaced by another mod and will load at startup", modelLocation); + this.forceLoadModel = true; + this.loadModel(modelLocation); + this.forceLoadModel = false; + } + ci.cancel(); + } + } + + private boolean trustedResourcePack(PackResources pack) { + return pack instanceof VanillaPackResources || + pack instanceof ClientPackSource || + pack instanceof FolderPackResources || + pack instanceof FilePackResources; + } + + @Redirect(method = "", at = @At(value = "INVOKE", target = "Ljava/util/stream/Stream;collect(Ljava/util/stream/Collector;)Ljava/lang/Object;", ordinal = 0)) + private Object collectExtraTextures(Stream instance, Collector arCollector) { + Set materialsSet = new ObjectOpenHashSet<>(instance.collect(Collectors.toSet())); + ModelBakeryHelpers.gatherModelMaterials(this.resourceManager, this::trustedResourcePack, materialsSet, + blockStateFiles, modelFiles, this.missingModel, json -> BlockModel.GSON.fromJson(json, BlockModel.class), + this::getModel); + /* take every texture from these folders (1.19.3+ emulation) */ + String[] extraFolders = new String[] { + "block", + "blocks", + "item", + "items", + "bettergrass" + }; + for(String folder : extraFolders) { + Collection textureLocations = this.resourceManager.listResources("textures/" + folder, p -> p.endsWith(".png")); + for(ResourceLocation rl : textureLocations) { + if(rl.getNamespace().equals("assets")) { + /* buggy pack, correct path */ + int slashIndex = rl.getPath().indexOf('/'); + String actualNamespace = rl.getPath().substring(0, slashIndex); + String actualPath = rl.getPath().substring(slashIndex + 1); + rl = new ResourceLocation(actualNamespace, actualPath); + } + ResourceLocation texLoc = new ResourceLocation(rl.getNamespace(), rl.getPath().substring(9, rl.getPath().length() - 4)); + materialsSet.add(new Material(TextureAtlas.LOCATION_BLOCKS, texLoc)); + } + } + blockStateFiles = null; + modelFiles = null; + return materialsSet; + } + @Inject(method = "uploadTextures", at = @At(value = "FIELD", target = "Lnet/minecraft/client/resources/model/ModelBakery;topLevelModels:Ljava/util/Map;", ordinal = 0), cancellable = true) private void skipBake(TextureManager resourceManager, ProfilerFiller profiler, CallbackInfoReturnable cir) { profiler.pop(); @@ -162,15 +279,21 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { } }; // discard unwrapped models - int oldSize = this.unbakedCache.size(); Predicate> isVanillaModel = entry -> entry.getValue() instanceof BlockModel || entry.getValue() instanceof MultiVariant || entry.getValue() instanceof MultiPart; - // bake indigo models - this.topLevelModels.entrySet().forEach((entry) -> { - if(!isVanillaModel.test(entry)) - this.bake(entry.getKey(), BlockModelRotation.X0_Y0); - }); this.unbakedCache.entrySet().removeIf(isVanillaModel); - ModernFix.LOGGER.info("{} models evicted, {} custom models loaded permanently", oldSize - this.unbakedCache.size(), this.unbakedCache.size()); + this.topLevelModels.entrySet().removeIf(isVanillaModel); + // bake indigo models + Stopwatch watch = Stopwatch.createStarted(); + this.topLevelModels.forEach((key, value) -> { + try { + this.bake(key, BlockModelRotation.X0_Y0); + } catch(RuntimeException e) { + ModernFix.LOGGER.error("Model {} failed to bake", key, e); + } + }); + watch.stop(); + ModernFix.LOGGER.info("Early model bake took {}", watch); + ModernFix.LOGGER.info("{} unbaked models, {} baked models loaded permanently", this.unbakedCache.size(), this.bakedCache.size()); this.unbakedCache = new LayeredForwardingMap<>(new Map[] { this.unbakedCache, mutableBackingMap }); this.bakedTopLevelModels = new DynamicBakedModelProvider((ModelBakery)(Object)this, bakedCache); @@ -194,7 +317,6 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { return unbakedCache.get(rl); } - /** * @author embeddedt * @reason synchronize @@ -281,7 +403,19 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery { if(debugDynamicModelLoading) LOGGER.info("Baking {}", arg); UnbakedModel iunbakedmodel = this.getModel(arg); - iunbakedmodel.getMaterials(this::getModel, new HashSet<>()); + Set> errorSet = new HashSet<>(); + Collection theMaterials = iunbakedmodel.getMaterials(this::getModel, errorSet); + /* check if sprites are actually present */ + TextureAtlasSprite missingSprite = this.atlasSet.getAtlas(TextureAtlas.LOCATION_BLOCKS).getSprite(MissingTextureAtlasSprite.getLocation()); + for(Material m : theMaterials) { + if(m.atlasLocation().equals(TextureAtlas.LOCATION_BLOCKS)) { + TextureAtlasSprite sprite = this.atlasSet.getAtlas(TextureAtlas.LOCATION_BLOCKS).getSprite(m.texture()); + if(sprite == missingSprite && !m.texture().equals(MissingTextureAtlasSprite.getLocation())) + ModernFix.LOGGER.warn("Texture {} is not present in blocks atlas", m.texture()); + } + } + errorSet.stream().filter(pair -> !pair.getSecond().equals(MISSING_MODEL_LOCATION_STRING)).forEach(pair -> LOGGER.warn("Unable to resolve texture reference: {} in {}", pair.getFirst(), pair.getSecond())); + if(iunbakedmodel == missingModel && debugDynamicModelLoading) LOGGER.warn("Model {} not present", arg); BakedModel ibakedmodel = null;